diff --git a/server-rs/crates/platform-tripo/src/common/client.rs b/server-rs/crates/platform-tripo/src/common/client.rs index d74e331b5..b0b8e558d 100644 --- a/server-rs/crates/platform-tripo/src/common/client.rs +++ b/server-rs/crates/platform-tripo/src/common/client.rs @@ -1,10 +1,11 @@ use std::time::Duration; +use shared_contracts::model3d::common::Model3dTaskStatus; use tripo3d_sdk::TripoClient; use super::{ - TripoDownloadedArtifact, TripoError, TripoSettings, TripoTaskHandle, TripoTaskSnapshot, - TripoUrl, map_task, validate_task_id, + TripoDownloadedArtifact, TripoError, TripoSettings, TripoTaskHandle, TripoTaskOutput, + TripoTaskSnapshot, TripoUrl, map_task, validate_task_id, }; pub struct TripoProviderClient { @@ -47,18 +48,10 @@ impl TripoProviderClient { &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(), - })?; + let output = completed_output(task)?; // TODO SDK upstream: expose a streaming artifact API; then replace this // provider-side reqwest client with the SDK stream and remove the duplicate downloader. - self.download_artifact(&handle.task_id, output.model_url()) + self.download_artifact(&task.handle.task_id, output.model_url()) .await } @@ -70,16 +63,8 @@ impl TripoProviderClient { &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()) + let output = completed_output(task)?; + self.download_artifact(&task.handle.task_id, output.rendered_image_url()) .await } @@ -149,6 +134,27 @@ fn download_backoff(attempt: u32) -> Duration { Duration::from_millis(250u64.saturating_mul(2u64.saturating_pow(attempt.min(6)))) } +/// 取完成态产物:下载入口只接受已完成且带输出的 task 快照。 +/// +/// `TripoTaskSnapshot` 字段全部公开,「完成态才有 output」只是 `map_task` 的约定, +/// 不是类型保证;调用方可以构造出 status 与 output 不一致的快照,所以这里重新校验, +/// 避免把失败 / 取消的任务当成成功产物下载。 +fn completed_output(task: &TripoTaskSnapshot) -> Result<&TripoTaskOutput, TripoError> { + validate_task_id(&task.handle.task_id)?; + if task.status != Model3dTaskStatus::Completed { + return Err(TripoError::OutputSchema { + task_id: task.handle.task_id.clone(), + message: format!("task is not completed (status {:?})", task.status), + }); + } + task.output + .as_ref() + .ok_or_else(|| TripoError::OutputSchema { + task_id: task.handle.task_id.clone(), + message: "task has no completed output".into(), + }) +} + fn transport_error_kind(error: &reqwest::Error) -> &'static str { if error.is_timeout() { "timeout"