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, axum::extract::Extension(request_context): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, 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, axum::extract::Extension(request_context): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, 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, axum::extract::Extension(request_context): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, 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, axum::extract::Extension(request_context): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, 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 { 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( request_context: &RequestContext, payload: Result, JsonRejection>, ) -> Result, Response> { payload.map_err(|rejection| { AppError::from_status(StatusCode::BAD_REQUEST) .with_message(format!("请求体 JSON 不合法:{rejection}")) .into_response_with_context(Some(request_context)) }) }