Files
Genarrative/server-rs/crates/api-server/src/hyper3d_generation.rs
2026-05-26 13:18:13 +08:00

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))
})
}