产物下载前校验任务确实处于完成态

- download_model 与 download_rendered_image 原先只看 output 是否存在,快照字段全公开、status 与 output 可以被人为构造得不一致
- 两个入口统一走 completed_output:先校验 task_id,再要求 status=Completed 且有输出,否则按输出结构错误失败
This commit is contained in:
2026-09-21 15:14:00 +08:00
parent 6724f8b707
commit 694a3c6f6c
@@ -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<TripoDownloadedArtifact, TripoError> {
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<TripoDownloadedArtifact, TripoError> {
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"