3D 资源没有预览图时封面留空,不再拿模型对象顶上去
- api-server:`project_editor_client_media_src` 对 `model3d` 在没有预览(`thumbnail_src` 与存储的 `image_src` 都缺)时直接返回空,不再回落到模型对象;用例改成断言空串,并补「存储的 image_src 是空白」一例 - docs/adr 0003:写明这是唯一允许「有对象但 `imageSrc` 为空」的类别,消费方按空值显示占位;被否决项里补上「拿模型对象冒充 imageSrc 的投影回落」 - docs/technical Tripo 集成方案:资源表说明改成「有预览才填」,验收项 11 改成「预览缺失时留空、不得回落成模型对象」 - 画布侧 `Model3dViewerModel` 的注释同步:历史布局里仍可能留着指向模型本体的地址,图片元素不能加载它
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
|
||||
状态:已接受
|
||||
|
||||
3D 生成结果在资源行里同时持有三个字段:`objectKey` 是模型本体,`imageSrc` 与 `thumbnailSrc` 是预览图。客户端可见媒体投影不能再沿用「`objectKey` 非空就以它充当 `imageSrc`」的历史口径,否则画布卡片、素材缩略图与后台都会把 `.glb` 交给图片元素加载;因此投影改为按 `asset_kind` 分叉:`model3d` 只把预览图投影给客户端(依次取 `thumbnail_src`、`image_src`),模型本体只留在 `objectKey` 里交给 3D 查看器,其它类别保持原样,两个来源都缺失时才回落到历史口径。
|
||||
3D 生成结果在资源行里同时持有三个字段:`objectKey` 是模型本体,`imageSrc` 与 `thumbnailSrc` 是预览图。客户端可见媒体投影不能再沿用「`objectKey` 非空就以它充当 `imageSrc`」的历史口径,否则画布卡片、素材缩略图与后台都会把 `.glb` 交给图片元素加载;因此投影改为按 `asset_kind` 分叉:`model3d` 只把预览图投影给客户端(依次取 `thumbnail_src`、`image_src`),模型本体只留在 `objectKey` 里交给 3D 查看器;`model3d` 的两个预览来源都缺失时 `imageSrc` 留空(消费方显示占位图),这是唯一允许「有对象但 `imageSrc` 为空」的类别。其它类别保持原样,两个来源都缺失时仍回落到历史口径。
|
||||
|
||||
模型格式的真相同样只保留一处:后端写入 `asset_object.content_type` 之前按字节魔数识别真实产物(GLB 的 `glTF` 头、FBX 的 `Kaydara FBX Binary` 头、glTF 的 JSON 正文),对象键扩展名由该内容类型派生而不是反向推断。客户端不复制这份格式清单,也不在画布层按扩展名预先判定放行:画布只认「`assetKind=model3d` 且 `objectKey` 非空」,真正的格式判定跟着解析器走——`packages/model3d-viewer` 依次采信读接口声明的 `Content-Type`、字节魔数、地址扩展名,格式在该包里是可选输入,三者都判不出来时以 `unsupported-format` 失败,并把原因与支持的格式清单显示在模态里,而不是把入口藏起来去让用户猜。
|
||||
|
||||
被否决的做法:让画布或宿主外壳按对象键扩展名自行判断放行(客户端第二份格式清单,查看器扩了格式后画布仍会继续禁用入口);给资源表新增 `model_format` 列(同一真相两个来源,且 provider 上报的格式不可信);预览图缺失时把模型文件直接喂给图片元素(静默坏图、无法诊断,也说不出「该资源没有可预览的模型」这类错误信息);在画布侧补「`assetKind` 为空但对象键像模型」的扩展名兜底(这种资源本就不该出现,兜底只会掩盖后端投影缺陷)。
|
||||
被否决的做法:让画布或宿主外壳按对象键扩展名自行判断放行(客户端第二份格式清单,查看器扩了格式后画布仍会继续禁用入口);给资源表新增 `model_format` 列(同一真相两个来源,且 provider 上报的格式不可信);预览图缺失时把模型文件直接喂给图片元素(静默坏图、无法诊断,也说不出「该资源没有可预览的模型」这类错误信息),以及拿模型对象冒充 `imageSrc` 的投影回落(同一个坏图问题,只是从后端发生);在画布侧补「`assetKind` 为空但对象键像模型」的扩展名兜底(这种资源本就不该出现,兜底只会掩盖后端投影缺陷)。
|
||||
|
||||
后果:下载 3D 资源时按真实响应头的 MIME 命名、对象键扩展名兜底、最后回落 `glb`,导出目录用 `3d_models/` 与图片、其它媒体分开;类型角标、缩略图和后台渲染器只要读 `imageSrc` 就能拿到预览图,不必各自识别媒体语义;查看器包单模型体积上限 64 MiB,超限与 `webgl-unavailable` 都作为失败原因展示给用户,而不是静默失败。
|
||||
后果:下载 3D 资源时按真实响应头的 MIME 命名、对象键扩展名兜底、最后回落 `glb`,导出目录用 `3d_models/` 与图片、其它媒体分开;类型角标、缩略图和后台渲染器只要读 `imageSrc` 就能拿到预览图(为空即显示占位),不必各自识别媒体语义;查看器包单模型体积上限 64 MiB,超限与 `webgl-unavailable` 都作为失败原因展示给用户,而不是静默失败。
|
||||
|
||||
多文件模型不在支持范围内:站内 3D 产物是单文件的(`.glb` 自带几何与贴图,`.fbx` 按媒体内嵌处理),不假设会出现「模型文件 + 同目录 buffer / 贴图」这种产物,最多是「模型 + 一个 bin」。因此 `packages/model3d-viewer` 里把模型地址的签名 query 顺带补到同目录兄弟资源地址上的那段逻辑(`resolveModel3dViewerResourceUrl`)只是兜底,不代表查看器支持多文件模型:签名按单个文件签发时,贴到另一个文件上会被云端以签名不匹配拒绝。将来真要支持多文件模型,正确做法是让宿主为每个兄弟文件分别换取各自的临时地址,而不是继续扩展这段拼接。
|
||||
|
||||
|
||||
@@ -157,12 +157,12 @@ price = <端点段>.versionPrices[modelVersion][texture ? "texture" : "noTexture
|
||||
```text
|
||||
assetKind = "model3d"
|
||||
objectKey = 模型对象
|
||||
imageSrc = 预览图稳定引用(该列非空,必须填预览)
|
||||
imageSrc = 预览图稳定引用(有预览才填;没有预览时留空,不拿模型对象顶封面)
|
||||
thumbnailSrc = 预览图
|
||||
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`)的情况,不负责纠正一个「认得出来但不对」的声明。
|
||||
模型格式与体积由 `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`)的情况,不负责纠正一个「认得出来但不对」的声明。
|
||||
|
||||
`external_generation_job.phase` 的取值集合不变,仍只允许现有两种执行阶段;阶段文案由 api-server 映射,不扩展 schema 常量。
|
||||
|
||||
@@ -201,7 +201,7 @@ width/height = 预览图像素尺寸
|
||||
8. provider 成功但落库失败时 job 失败、该 attempt 退款,checkpoint 保留供对账。
|
||||
9. worker 落库时构造的完成结果按端点严格类型化,只含模型与预览的正式资源引用,不含 provider task ID 与带签名的临时 URL;标准消费者的队列结果不透传它(与其它画布生成任务一致,结果靠画布 / 素材读回)。
|
||||
10. 素材库落点产出 `assetId`;项目资源落点产出 `resourceId`,且 `assetKind=model3d`、模型与预览的 `AssetObject.content_type` 都是按字节识别出的 mime(预览不再一律写成 `image/webp`)、`content_length` 与实际对象一致。
|
||||
11. 客户端媒体投影对 `model3d` 只暴露预览图:`imageSrc` / `thumbnailSrc` 指向预览对象、`objectKey` 指向模型本体;预览缺失时才回落到模型对象,且该回落不得让图片入口加载模型文件。
|
||||
11. 客户端媒体投影对 `model3d` 只暴露预览图:`imageSrc` / `thumbnailSrc` 指向预览对象、`objectKey` 指向模型本体;预览缺失时 `imageSrc` 留空(消费方显示占位图),不得回落成模型对象——那只会让图片入口去加载 `.glb`。
|
||||
12. 格式判定的顺序在查看器包内可被用例钉住:声明 `Content-Type` 优先于字节魔数、字节魔数优先于地址扩展名;三者都判不出来时模态展示 `unsupported-format` 原因与支持的格式清单,画布侧不做任何格式判断、也不在 `assetKind` 为空时按扩展名兜底。
|
||||
13. 真实 Provider smoke 覆盖 text-to-model 与 image-to-model 各一次,下载字节数与 `Content-Length` 一致。
|
||||
14. 定向测试、`cargo check`、`npm run check:spacetime-schema`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check` 全部通过。
|
||||
|
||||
@@ -11362,8 +11362,10 @@ pub(crate) const EDITOR_MODEL3D_ASSET_KIND: &str = "model3d";
|
||||
/// - `model3d`:`imageSrc` 与 `thumbnailSrc` 都是预览图,模型本体只留在 `objectKey`。
|
||||
/// 预览依次取 `thumbnail_src`、存储的 `image_src`。模型与预览是两个对象,不能让
|
||||
/// `object_key` 覆盖 `image_src`,否则画布卡片、素材缩略图和后台都会把模型文件当图片加载。
|
||||
/// - 其它类别:沿用「`object_key` 非空即以它为 `imageSrc`」的历史口径。
|
||||
/// - 两个来源都缺失时回落到历史口径,保证「有对象就有 `imageSrc`」的不变量不退化。
|
||||
/// 两个来源都缺失时 `imageSrc` 留空:这是唯一允许「有对象但 `imageSrc` 为空」的类别,
|
||||
/// 消费方按空值渲染占位图,不拿 `.glb` 的位置冒充封面。
|
||||
/// - 其它类别:沿用「`object_key` 非空即以它为 `imageSrc`」的历史口径,两个来源都缺失时
|
||||
/// 回落到它,保证这些类别的「有对象就有 `imageSrc`」不变量不退化。
|
||||
fn project_editor_client_media_src(
|
||||
asset_kind: Option<&str>,
|
||||
image_src: String,
|
||||
@@ -11379,9 +11381,10 @@ fn project_editor_client_media_src(
|
||||
let image_src = image_src.trim();
|
||||
(!image_src.is_empty()).then(|| image_src.to_string())
|
||||
});
|
||||
if let Some(preview_src) = preview_src {
|
||||
return normalize_editor_record_media_src(preview_src, None);
|
||||
}
|
||||
return preview_src
|
||||
.map(|preview_src| normalize_editor_record_media_src(preview_src, None))
|
||||
// 没有预览就留空:模型文件不是封面,宁可由消费方显示占位图。
|
||||
.unwrap_or_default();
|
||||
}
|
||||
normalize_editor_record_media_src(image_src, object_key)
|
||||
}
|
||||
@@ -14413,8 +14416,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 3D 没有预览图时封面留空:模型文件(.glb)不是图片,拿它顶封面只会得到裂图。
|
||||
/// 这是唯一允许「有对象但 imageSrc 为空」的类别,前端按空值渲染占位。
|
||||
#[test]
|
||||
fn editor_client_media_projection_falls_back_when_model3d_has_no_preview() {
|
||||
fn editor_client_media_projection_leaves_model3d_cover_empty_without_preview() {
|
||||
assert_eq!(
|
||||
project_editor_client_media_src(
|
||||
Some("model3d"),
|
||||
@@ -14422,7 +14427,16 @@ mod tests {
|
||||
Some("generated-character-drafts/editor/model3d/task-1/model.glb"),
|
||||
Some(" "),
|
||||
),
|
||||
"/generated-character-drafts/editor/model3d/task-1/model.glb"
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
project_editor_client_media_src(
|
||||
Some("model3d"),
|
||||
" ".to_string(),
|
||||
Some("generated-character-drafts/editor/model3d/task-1/model.glb"),
|
||||
None,
|
||||
),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ const MODEL3D_VIEWER_SUPPORTED_FORMAT_TEXT =
|
||||
/**
|
||||
* 判断一个媒体地址是否其实指向模型本体。
|
||||
*
|
||||
* 后端投影正常情况下把 `imageSrc` 指到预览图;预览图缺失时它会回落到模型对象,
|
||||
* 此时图片元素不能去加载模型文件,必须改显示 3D 占位。
|
||||
* 后端投影只把预览图放进 `imageSrc`(没有预览就留空,不再拿模型对象顶封面),
|
||||
* 但历史布局与旧快照里可能还留着指向模型本体的地址:图片元素不能去加载模型文件,
|
||||
* 必须改显示 3D 占位。
|
||||
*/
|
||||
export function isModel3dFileSource(value: string | null | undefined) {
|
||||
return resolveModel3dViewerFormat({ url: value }) !== null;
|
||||
|
||||
Reference in New Issue
Block a user