3D 产物登记失败时删掉已经传上去的对象

- storage:新增 `discard_model3d_artifact`,按对象键删除刚写入的产物;OSS 未配置或删除失败都只记警告并带上对象键,方便人工清理
- worker:预览图上传失败时删掉已传的模型对象;登记失败时把模型与预览图两个对象都删掉;`persist_result` 改成接收引用,好让调用方在失败后还能按对象键清理
- docs/technical:写明「上传成功但后续失败就删掉对象,不接受失败就烂在 bucket 里」
This commit is contained in:
2026-09-24 20:46:42 +08:00
parent 8af02daeea
commit 2aa59328de
3 changed files with 66 additions and 14 deletions
@@ -164,7 +164,7 @@ width/height = 预览图像素尺寸
模型格式与体积由 `AssetObject` 承担(`content_type`、`content_length`、`content_hash`),因此不新增 `model_format`、`size_bytes`、`poly_count` 等列。两个槽位的 `content_type` 都在写入前按字节识别真实产物:模型按魔数认 GLB 的 `glTF` 头、FBX 的 `Kaydara FBX Binary` 头与 glTF 的 JSON 正文,预览按魔数认 PNG / JPEG / WebP;不直接采信 provider 返回或下载响应头声明的类型,对象键扩展名由该内容类型派生。只有字节判不出来时才采信声明值,且声明值必须落在该槽位的白名单内(模型:`model/gltf-binary`、`model/gltf+json`、`model/fbx`、`application/x-fbx`;预览:`image/png`、`image/jpeg`、`image/jpg`、`image/webp`);两边都对不上就直接按上游内容不合法失败并退款,不做任何兜底 —— 放行 `text/plain` 这类未知类型只会落成「错的 content type + 拼出来的扩展名」。客户端不复制这份格式清单:画布只按「`assetKind=model3d` 且 `objectKey` 非空」放行,能否交给 3D 查看器由 `packages/model3d-viewer` 判定,判据依次是读接口声明的 `Content-Type`、字节魔数、地址扩展名,全判不出来即按 `unsupported-format` 报错。这里的顺序是「信任后端检测结果」:`content_type` 在后端写入前已经按字节核过,所以客户端以声明为先;字节魔数只兜住声明缺失或认不出来(例如 `application/octet-stream`)的情况,不负责纠正一个「认得出来但不对」的声明。
产物对象由 api-server 按固定前缀 PUT 写入、再 HEAD 复核后才登记;PUT 带退避重试(最多 3 次尝试,退避 250 / 500 ms),只对传输 / 超时 / 408 / 429 / 5xx 重试,确定性 4xx 直接失败。3D job 最多只尝试一次,一次瞬时网络失败就会废掉一笔已经计费的生成,所以这条链路上的每个网络动作都要能重试。
产物对象由 api-server 按固定前缀 PUT 写入、再 HEAD 复核后才登记;PUT 带退避重试(最多 3 次尝试,退避 250 / 500 ms),只对传输 / 超时 / 408 / 429 / 5xx 重试,确定性 4xx 直接失败。3D job 最多只尝试一次,一次瞬时网络失败就会废掉一笔已经计费的生成,所以这条链路上的每个网络动作都要能重试。反过来,产物一旦上传成功、后续预览图上传或登记失败,就把已经传上去的对象删掉(删除失败只记警告,残留对象按日志人工清理):不接受“失败就烂在 bucket 里”。
`external_generation_job.phase` 的取值集合不变,仍只允许现有两种执行阶段;阶段文案由 api-server 映射,不扩展 schema 常量。