172 lines
6.7 KiB
Rust
172 lines
6.7 KiB
Rust
use axum::{
|
|
Json,
|
|
extract::{State, rejection::JsonRejection},
|
|
http::StatusCode,
|
|
response::Response,
|
|
};
|
|
use platform_hyper3d::{Hyper3dError, Hyper3dSettings, Hyper3dStatusHint};
|
|
use serde_json::{Value, json};
|
|
use shared_contracts::hyper3d as contract;
|
|
|
|
use crate::{
|
|
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
|
state::AppState,
|
|
};
|
|
|
|
pub async fn submit_hyper3d_text_to_model(
|
|
State(state): State<AppState>,
|
|
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
|
payload: Result<Json<contract::Hyper3dTextToModelRequest>, JsonRejection>,
|
|
) -> Result<Json<Value>, Response> {
|
|
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
|
let settings = require_hyper3d_settings(&state)
|
|
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
|
platform_hyper3d::submit_text_to_model(&settings, payload)
|
|
.await
|
|
.map(|payload| json_success_body(Some(&request_context), payload))
|
|
.map_err(|error| {
|
|
map_platform_hyper3d_error(error).into_response_with_context(Some(&request_context))
|
|
})
|
|
}
|
|
|
|
pub async fn submit_hyper3d_image_to_model(
|
|
State(state): State<AppState>,
|
|
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
|
payload: Result<Json<contract::Hyper3dImageToModelRequest>, JsonRejection>,
|
|
) -> Result<Json<Value>, Response> {
|
|
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
|
let settings = require_hyper3d_settings(&state)
|
|
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
|
platform_hyper3d::submit_image_to_model(&settings, payload)
|
|
.await
|
|
.map(|payload| json_success_body(Some(&request_context), payload))
|
|
.map_err(|error| {
|
|
map_platform_hyper3d_error(error).into_response_with_context(Some(&request_context))
|
|
})
|
|
}
|
|
|
|
pub async fn get_hyper3d_task_status(
|
|
State(state): State<AppState>,
|
|
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
|
payload: Result<Json<contract::Hyper3dTaskStatusRequest>, JsonRejection>,
|
|
) -> Result<Json<Value>, Response> {
|
|
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
|
let settings = require_hyper3d_settings(&state)
|
|
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
|
platform_hyper3d::query_task_status(&settings, payload)
|
|
.await
|
|
.map(|payload| json_success_body(Some(&request_context), payload))
|
|
.map_err(|error| {
|
|
map_platform_hyper3d_error(error).into_response_with_context(Some(&request_context))
|
|
})
|
|
}
|
|
|
|
pub async fn get_hyper3d_downloads(
|
|
State(state): State<AppState>,
|
|
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
|
payload: Result<Json<contract::Hyper3dDownloadRequest>, JsonRejection>,
|
|
) -> Result<Json<Value>, Response> {
|
|
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
|
let settings = require_hyper3d_settings(&state)
|
|
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
|
platform_hyper3d::query_downloads(&settings, payload)
|
|
.await
|
|
.map(|payload| json_success_body(Some(&request_context), payload))
|
|
.map_err(|error| {
|
|
map_platform_hyper3d_error(error).into_response_with_context(Some(&request_context))
|
|
})
|
|
}
|
|
|
|
fn require_hyper3d_settings(state: &AppState) -> Result<Hyper3dSettings, AppError> {
|
|
let base_url = state.config.hyper3d_base_url.trim().trim_end_matches('/');
|
|
if base_url.is_empty() {
|
|
return Err(map_platform_hyper3d_error(Hyper3dError::invalid_config(
|
|
"HYPER3D_BASE_URL 未配置",
|
|
"Hyper3D Rodin 服务地址未配置,请设置 HYPER3D_BASE_URL 或 RODIN_BASE_URL 后重启 api-server。",
|
|
)));
|
|
}
|
|
|
|
let api_key = state
|
|
.config
|
|
.hyper3d_api_key
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
map_platform_hyper3d_error(Hyper3dError::invalid_config(
|
|
"HYPER3D_API_KEY 未配置",
|
|
"Hyper3D Rodin API Key 未配置,请在本地私密环境设置 HYPER3D_API_KEY 或 RODIN_API_KEY 后重启 api-server。",
|
|
))
|
|
})?;
|
|
|
|
Ok(Hyper3dSettings {
|
|
base_url: base_url.to_string(),
|
|
api_key: api_key.to_string(),
|
|
request_timeout_ms: state.config.hyper3d_model_request_timeout_ms.max(1),
|
|
})
|
|
}
|
|
|
|
fn map_platform_hyper3d_error(error: Hyper3dError) -> AppError {
|
|
let status = match error.status_hint() {
|
|
Hyper3dStatusHint::BadRequest => StatusCode::BAD_REQUEST,
|
|
Hyper3dStatusHint::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
|
|
Hyper3dStatusHint::BadGateway => StatusCode::BAD_GATEWAY,
|
|
Hyper3dStatusHint::GatewayTimeout => StatusCode::GATEWAY_TIMEOUT,
|
|
};
|
|
let mut details = json!({
|
|
"provider": error.provider(),
|
|
"message": error.message(),
|
|
});
|
|
match &error {
|
|
Hyper3dError::InvalidConfig { reason, .. } => {
|
|
details["reason"] = json!(reason);
|
|
}
|
|
Hyper3dError::InvalidRequest { field, allowed, .. } => {
|
|
details["field"] = json!(field);
|
|
details["allowed"] = json!(allowed);
|
|
}
|
|
Hyper3dError::Request {
|
|
endpoint,
|
|
timeout,
|
|
connect,
|
|
request,
|
|
body,
|
|
status_code,
|
|
source,
|
|
..
|
|
} => {
|
|
details["endpoint"] = json!(endpoint);
|
|
details["timeout"] = json!(timeout);
|
|
details["connect"] = json!(connect);
|
|
details["request"] = json!(request);
|
|
details["body"] = json!(body);
|
|
details["status"] = json!(status_code);
|
|
details["source"] = json!(source);
|
|
}
|
|
Hyper3dError::Upstream {
|
|
upstream_status,
|
|
raw_excerpt,
|
|
..
|
|
} => {
|
|
details["status"] = json!(upstream_status);
|
|
details["rawExcerpt"] = json!(raw_excerpt);
|
|
}
|
|
Hyper3dError::ResponseParse { raw_excerpt, .. } => {
|
|
details["rawExcerpt"] = json!(raw_excerpt);
|
|
}
|
|
Hyper3dError::MissingField { .. } => {}
|
|
}
|
|
AppError::from_status(status).with_details(details)
|
|
}
|
|
|
|
fn parse_json_payload<T>(
|
|
request_context: &RequestContext,
|
|
payload: Result<Json<T>, JsonRejection>,
|
|
) -> Result<Json<T>, Response> {
|
|
payload.map_err(|rejection| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message(format!("请求体 JSON 不合法:{rejection}"))
|
|
.into_response_with_context(Some(request_context))
|
|
})
|
|
}
|