From a8799adf5d167fa0b8221ea6a5d7dd42a23cb7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 21 Sep 2026 14:06:47 +0800 Subject: [PATCH] =?UTF-8?q?platform-tripo=E6=94=AF=E6=8C=81=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E4=B8=8E=E9=A2=84=E8=A7=88=E5=9B=BE?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=20-=20=E4=BA=A7=E7=89=A9=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=94=B9=E5=90=8D=E4=B8=BA=20TripoDownloaded?= =?UTF-8?q?Artifact=EF=BC=8C=E6=A8=A1=E5=9E=8B=E4=B8=8E=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=9B=BE=E5=85=B1=E7=94=A8=E5=90=8C=E4=B8=80=E6=9D=A1=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E8=AF=BB=E5=8F=96=E8=B7=AF=E5=BE=84=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20download=5Frendered=5Fimage=EF=BC=8C=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=20task=20=E7=BB=93=E6=9E=9C=E9=87=8C=E7=9A=84?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E5=9B=BE=E5=9C=B0=E5=9D=80=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20upload=5Fimage=EF=BC=8C=E6=8A=8A=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=AD=97=E8=8A=82=E4=B8=8A=E4=BC=A0=20provider=20=E6=8D=A2=20f?= =?UTF-8?q?ile=5Ftoken=20-=20=E5=9B=BE=E7=89=87=E8=BE=93=E5=85=A5=E6=94=B6?= =?UTF-8?q?=E6=95=9B=E4=B8=BA=20TripoImageInput=20=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=EF=BC=8C=E5=8F=AA=E6=9C=89=20file=5Ftoken=20=E4=B8=8E=E5=85=AC?= =?UTF-8?q?=E7=BD=91=E5=9C=B0=E5=9D=80=E4=B8=A4=E7=A7=8D=E5=BD=A2=E6=80=81?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=8E=A5=E5=8F=97=E8=A3=B8=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=20-=20smoke=20=E7=A4=BA=E4=BE=8B=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=E6=96=B0=E7=9A=84=E5=9B=BE=E7=89=87=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examples/tripo_generation_smoke.rs | 4 +- .../platform-tripo/src/common/client.rs | 33 +++++-- .../crates/platform-tripo/src/common/mod.rs | 2 +- .../crates/platform-tripo/src/common/types.rs | 15 ++- .../src/image_to_model/client.rs | 91 ++++++++++++++++--- .../platform-tripo/src/image_to_model/mod.rs | 1 + server-rs/crates/platform-tripo/src/lib.rs | 4 +- 7 files changed, 126 insertions(+), 24 deletions(-) diff --git a/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs b/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs index cf5747e19..6fd8f1b0b 100644 --- a/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs +++ b/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs @@ -14,7 +14,7 @@ use std::{env, time::Duration}; -use platform_tripo::TripoProviderClient; +use platform_tripo::{TripoImageInput, TripoProviderClient}; use shared_contracts::model3d::{ common::{Model3dModelVersion, Model3dTaskStatus, Model3dTextureQuality}, image_to_model::Model3dImageToModelParams, @@ -73,7 +73,7 @@ async fn main() -> Result<(), Box> { let image = client .submit_image_to_model( - SAMPLE_IMAGE_URL, + &TripoImageInput::PublicUrl(SAMPLE_IMAGE_URL.to_string()), &Model3dImageToModelParams { model: Model3dModelVersion::H31, enable_image_autofix: None, diff --git a/server-rs/crates/platform-tripo/src/common/client.rs b/server-rs/crates/platform-tripo/src/common/client.rs index e4d03bf75..d74e331b5 100644 --- a/server-rs/crates/platform-tripo/src/common/client.rs +++ b/server-rs/crates/platform-tripo/src/common/client.rs @@ -3,8 +3,8 @@ use std::time::Duration; use tripo3d_sdk::TripoClient; use super::{ - TripoDownloadedModel, TripoError, TripoSettings, TripoTaskHandle, TripoTaskSnapshot, TripoUrl, - map_task, validate_task_id, + TripoDownloadedArtifact, TripoError, TripoSettings, TripoTaskHandle, TripoTaskSnapshot, + TripoUrl, map_task, validate_task_id, }; pub struct TripoProviderClient { @@ -46,7 +46,7 @@ impl TripoProviderClient { pub async fn download_model( &self, task: &TripoTaskSnapshot, - ) -> Result { + ) -> Result { let handle = &task.handle; validate_task_id(&handle.task_id)?; let output = task @@ -62,11 +62,32 @@ impl TripoProviderClient { .await } + /// 下载 provider 渲染出的预览图。 + /// + /// 预览图和模型是同一次 task 结果里的两个产物,共用同一条产物下载链路; + /// 命名按“预览图”而不是“渲染图”,避免调用方把它当成模型对象。 + pub async fn download_rendered_image( + &self, + task: &TripoTaskSnapshot, + ) -> Result { + let handle = &task.handle; + validate_task_id(&handle.task_id)?; + let output = task + .output + .as_ref() + .ok_or_else(|| TripoError::OutputSchema { + task_id: handle.task_id.clone(), + message: "task has no completed model output".into(), + })?; + self.download_artifact(&handle.task_id, output.rendered_image_url()) + .await + } + async fn download_artifact( &self, task_id: &str, url: &TripoUrl, - ) -> Result { + ) -> Result { let total_attempts = self.artifact_retries.saturating_add(1); let mut last_error = None; @@ -88,7 +109,7 @@ impl TripoProviderClient { &self, task_id: &str, url: &TripoUrl, - ) -> Result { + ) -> Result { let response = self .artifact_client .get(url.as_str()) @@ -115,7 +136,7 @@ impl TripoProviderClient { .and_then(|value| value.to_str().ok()) .map(str::to_owned); let content_length = response.content_length(); - Ok(TripoDownloadedModel::new( + Ok(TripoDownloadedArtifact::new( url.clone(), content_type, content_length, diff --git a/server-rs/crates/platform-tripo/src/common/mod.rs b/server-rs/crates/platform-tripo/src/common/mod.rs index 810bd6da4..f5ad1094d 100644 --- a/server-rs/crates/platform-tripo/src/common/mod.rs +++ b/server-rs/crates/platform-tripo/src/common/mod.rs @@ -10,7 +10,7 @@ pub use config::TripoSettings; pub use error::{TripoError, TripoField, TripoValidationReason}; pub(crate) use mapping::map_task; pub use types::{ - TripoDownloadedModel, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, + TripoDownloadedArtifact, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, TripoTaskType, TripoUrl, }; pub(crate) use validation::{ diff --git a/server-rs/crates/platform-tripo/src/common/types.rs b/server-rs/crates/platform-tripo/src/common/types.rs index c639b8717..516e95f39 100644 --- a/server-rs/crates/platform-tripo/src/common/types.rs +++ b/server-rs/crates/platform-tripo/src/common/types.rs @@ -85,6 +85,15 @@ impl TripoTaskOutput { Self::MultiviewToModel(result) => &result.model_url, } } + + /// 预览图地址;三个端点都返回渲染图,缺失时按输出结构错误处理。 + pub(crate) fn rendered_image_url(&self) -> &TripoUrl { + match self { + Self::TextToModel(result) => &result.rendered_image_url, + Self::ImageToModel(result) => &result.rendered_image_url, + Self::MultiviewToModel(result) => &result.rendered_image_url, + } + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -99,7 +108,9 @@ pub struct TripoTaskSnapshot { pub completed_at: Option, } -pub struct TripoDownloadedModel { +/// provider 产物的下载句柄。模型与预览图共用同一条流式读取路径, +/// 因此类型名按“产物”而不是“模型”命名。 +pub struct TripoDownloadedArtifact { pub url: TripoUrl, pub content_type: Option, pub content_length: Option, @@ -108,7 +119,7 @@ pub struct TripoDownloadedModel { received: u64, } -impl TripoDownloadedModel { +impl TripoDownloadedArtifact { pub(crate) fn new( url: TripoUrl, content_type: Option, diff --git a/server-rs/crates/platform-tripo/src/image_to_model/client.rs b/server-rs/crates/platform-tripo/src/image_to_model/client.rs index 25b773fa2..c9ae60941 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/client.rs @@ -11,22 +11,91 @@ use crate::common::{ use super::validation::validate_image_to_model_params; +/// provider 能读取的图片输入形态。 +/// +/// 平台层只接受站内引用,所以调用方只会产出这两种形态:先上传拿 `file_token`, +/// 或给出公网可读地址。不提供裸字符串变体,避免 SDK 把字符串按前缀推断成 task_id。 +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TripoImageInput { + FileToken(String), + PublicUrl(String), +} + +impl TripoImageInput { + fn to_sdk_file_input(&self) -> Result { + let (value, kind) = match self { + Self::FileToken(value) => (value, "file_token"), + Self::PublicUrl(value) => (value, "url"), + }; + let value = value.trim(); + if value.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: format!("image {kind} must not be blank"), + }); + } + Ok(match self { + Self::FileToken(_) => FileInput::FileToken(value.to_string()), + Self::PublicUrl(_) => FileInput::Url(value.to_string()), + }) + } +} + impl TripoProviderClient { + /// 把图片字节上传到 provider,换取可直接用于生成任务的 `file_token`。 + /// + /// 站内图片保存在私有 OSS 上,provider 无法直接读取,所以图片输入统一先上传再提交, + /// 不把带签名的临时地址交给第三方。 + pub async fn upload_image( + &self, + bytes: Vec, + file_name: &str, + content_type: &str, + ) -> Result { + let file_name = file_name.trim(); + if file_name.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: "upload file name must not be blank".into(), + }); + } + if bytes.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: "upload body must not be empty".into(), + }); + } + let content_type = content_type.trim(); + let uploaded = self + .client + .upload_file( + bytes, + file_name.to_string(), + (!content_type.is_empty()).then_some(content_type), + ) + .await + .map_err(TripoError::from)?; + let token = uploaded.file_token.trim().to_string(); + if token.is_empty() { + return Err(TripoError::OutputSchema { + task_id: "upload".into(), + message: "upload response has no file_token".into(), + }); + } + Ok(TripoImageInput::FileToken(token)) + } + /// 图片输入由调用方解析后传入:provider 只负责把它交给 SDK, /// 站内资源 / 素材的归属校验属于平台层。 pub async fn submit_image_to_model( &self, - input: &str, + input: &TripoImageInput, params: &Model3dImageToModelParams, ) -> Result { - let input = input.trim(); - if input.is_empty() { - return Err(TripoError::InvalidParameters { - field: Some(TripoField::Input), - reason: TripoValidationReason::Required, - message: "input must not be blank".into(), - }); - } + let input = input.to_sdk_file_input()?; validate_image_to_model_params(params)?; let task_id = self @@ -38,9 +107,9 @@ impl TripoProviderClient { } } -fn to_sdk_params(input: &str, params: &Model3dImageToModelParams) -> ImageToModelParams { +fn to_sdk_params(input: FileInput, params: &Model3dImageToModelParams) -> ImageToModelParams { ImageToModelParams { - input: FileInput::from(input), + input, model: Some(params.model.as_str().to_owned()), enable_image_autofix: params.enable_image_autofix, model_seed: params.model_seed, diff --git a/server-rs/crates/platform-tripo/src/image_to_model/mod.rs b/server-rs/crates/platform-tripo/src/image_to_model/mod.rs index e07463772..a4a16bdda 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/mod.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/mod.rs @@ -2,4 +2,5 @@ mod client; pub mod result; mod validation; +pub use client::TripoImageInput; pub use validation::validate_image_to_model_params; diff --git a/server-rs/crates/platform-tripo/src/lib.rs b/server-rs/crates/platform-tripo/src/lib.rs index d8284a759..a409c3b36 100644 --- a/server-rs/crates/platform-tripo/src/lib.rs +++ b/server-rs/crates/platform-tripo/src/lib.rs @@ -9,9 +9,9 @@ pub mod multiview_to_model; pub mod text_to_model; pub use common::{ - TripoDownloadedModel, TripoError, TripoField, TripoProviderClient, TripoSettings, + TripoDownloadedArtifact, TripoError, TripoField, TripoProviderClient, TripoSettings, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, TripoTaskType, TripoUrl, TripoValidationReason, }; -pub use image_to_model::validate_image_to_model_params; +pub use image_to_model::{TripoImageInput, validate_image_to_model_params}; pub use text_to_model::validate_text_to_model_params;