3D 预览产物 content type 改为按字节识别
- storage.rs 新增 sniff_preview_content_type(PNG / JPEG / WebP),resolve_artifact_content_type 改为按槽位判定:预览与模型一样先嗅探字节,嗅不出才回落声明值(最终仍回落 image/webp) - 修掉 provider / CDN 不写或写错 Content-Type 时 PNG 预览被写成 image/webp 并传播到 OSS metadata、asset_object.content_type 与生成结果的问题;对象键扩展名随之正确 - 定向用例重写为「字节优先 / 未知回落」两组,替换原先钉住「预览不嗅探」的用例 - 技术方案与决策记录同步该口径,并记录与其它图片链路(bgfilter / VectorEngine / 生成图落 OSS 适配器)的一致性对照
This commit is contained in:
@@ -9403,3 +9403,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响面:`server-rs/crates/api-server/src/tripo3d/image_source.rs`(收敛分支、新增 `image_source_object_key_missing`、模块文档与定向用例)、[实施计划 Tripo生成Worker执行链路与API路由](../plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md)。
|
||||
- 验证方式:`cargo test --locked -p api-server tripo3d::image_source`(4 passed,含 `image_source_resolution_only_collapses_unavailable_references` 覆盖 400 / 404 收敛与 403 / 409 / 500 保留、`image_source_resolution_reports_missing_object_key_as_server_side_failure` 断言 502 与 reason)、`rustfmt --check`。
|
||||
- 关联文档:[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[实施计划 Tripo生成Worker执行链路与API路由](../plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md)。
|
||||
|
||||
## 2026-09-23 3D 预览产物 content type 改为按字节识别:只信 provider 声明的做法作废
|
||||
|
||||
- 背景:`tripo3d/storage.rs` 的 `Preview` 槽位原先不做字节嗅探,`application/octet-stream` 或空 `Content-Type` 一律写成 `image/webp` + `.webp` 文件名;而 `worker.rs` 的 `preview_dimensions` 早已用 `ImageReader::with_guessed_format()` 解出过真实格式(PNG / JPEG)却没往下传。Tripo 的渲染图 CDN 不写或写错 `Content-Type` 时,PNG 预览就会带着 `image/webp` 进 OSS metadata、`asset_object.content_type` 与生成结果的 `Model3dGeneratedArtifact.content_type`。
|
||||
- 决策:预览槽位与模型槽位同处一个判定点,按字节魔数识别 PNG / JPEG / WebP 后再决定 content type;识别范围外(含模型字节、非图片字节)保持既有回落(声明值可用就用声明值,否则 `image/webp`)。模型槽位行为不变。
|
||||
- 原因:**浏览器能容忍,不代表记录值可以继续说谎**。`<img>` 会按魔数嗅探,所以前端预览一直没坏;但 `assets.rs` 的 `"contentType"` 出参、素材库下载命名、后台与将来的服务端处理都信任 `asset_object.content_type`,对象键扩展名也由它派生。仓库同类图片链路的现行口径都是「字节是真相」——bgfilter 用 `guess_format` 定 mime 并把声明头只当一致性校验、VectorEngine 生成图走 `infer_image_mime_type`、生成图落 OSS 适配器的后缀与 PUT `content_type` 都取自嗅探结果;只有嗅不动的视频(`character_animation_assets.rs`)与回读已存对象的 HEAD 才信 header。本次把预览槽位对齐到这条口径,而不是删字段:删字段只会把 `image/webp` 换成同样错的 `application/octet-stream`,还要付 `Model3dGeneratedArtifact.content_type` 的破坏性契约变更代价。
|
||||
- 代价与取舍:多一次只读文件头的嗅探(不解码像素);worker 的 `preview_dimensions` 与 storage 的嗅探仍是两处调用,但前者是「必须能解码」的正交校验、后者是写库前的类型归一,判定点只有一个(`resolve_artifact_content_type`)。嗅探不到的格式不猜,仍回落 `image/webp`,因此不会因为一张少见格式的渲染图而失败退款。
|
||||
- 影响面:`server-rs/crates/api-server/src/tripo3d/storage.rs`(新增 `sniff_preview_content_type`、`resolve_artifact_content_type` 改为按槽位判定、定向用例重写为「字节优先 / 未知回落」两组)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)。
|
||||
- 验证方式:`cargo test --locked -p api-server tripo3d::`(56 passed,含 `preview_content_type_follows_bytes_over_declaration` 与 `preview_content_type_keeps_declaration_when_bytes_are_unknown`)、`rustfmt --check`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`。
|
||||
- 关联文档:[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[实施计划 Tripo生成API契约与数据模型](../plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md)。
|
||||
|
||||
@@ -51,7 +51,7 @@ source = { kind: "resource", resourceId } | { kind: "asset", assetId }
|
||||
|
||||
引用解析分两步,提交与执行互不代替:**提交时只做元数据预检**——按 `source.kind` 的分支做定点归属校验,确认记录类型与对象键存在,不读图片正文、不调用 provider,因此跨 owner、未登记、已删除与 `kind` 不符的引用都在扣费与入队之前返回 400;跨 owner、未登记与已删除对外收敛成同一句不可用,不泄漏他人 ID 是否存在,`kind` 与解析结果不符则单独报错(两个值都由调用方给出,不涉及探测他人数据);**worker 执行时重新确认同一事实**,通过后才读一次图片正文并上传换 `file_token`。预检只按主键定点查引用,不允许拉取当前用户的完整工程列表或素材库。
|
||||
|
||||
**结果落点**(两个端点都必填)与其它生成接口同形,是平坦的可选字段,不用 tagged enum;客户端不必为“落项目还是落素材库”多拼一层判别结构:
|
||||
**结果落点**(两个端点至少给一个)与其它生成接口同形,是平坦的可选字段,不用 tagged enum;客户端不必为“落项目还是落素材库”多拼一层判别结构:
|
||||
|
||||
```text
|
||||
projectId? 项目资源落点
|
||||
@@ -60,9 +60,11 @@ assetLabel? 素材名称,缺省用平台默认名
|
||||
canvasCompletion? 画布占位框回填,只在项目资源落点下生效
|
||||
```
|
||||
|
||||
`projectId` 与 `assetFolderId` 二选一且只能给一个:两个都不给、两个都给都拒绝;`assetLabel` 只在素材库落点下有默认值,`canvasCompletion` 与 `assetFolderId` 同现拒绝。不存在“都不给就默认落素材库”的口径。
|
||||
`projectId` 与 `assetFolderId` 至少给一个,并且允许同时给:两个都不给直接 400;同时给时画布资源行与素材行两条都写,与图片等画布生成工具完全一致(画布链路本来就会把“当前素材夹”一起发出去,见下)。`assetLabel` 只在素材库落点下生效,缺省用本次结果标题;`canvasCompletion` 是项目资源落点的回填载荷,“没有 `projectId` 却带占位框”被拒绝。不存在“都不给就默认落素材库”的口径。
|
||||
|
||||
落点预检与其它付费编辑器生成共用同一条路径:提交时调只读 `preflight_editor_generation_target_and_return`,按认证 owner 对给定的那个落点做定点归属校验(不再为了一个目录 ID 读整个素材库),跨 owner / 已删除 / 不存在收敛成同一句 400;预检传下去的就是 trim 后的原值,校验值与入队、落库的值逐字一致。`project` 与 owner 默认素材目录的归一由落库 procedure 负责,因此本期不做 API 层的 `folder-*` → 默认目录改写——那需要把归一值一路带进入队载荷,等真正打开素材库落点入口时再补。
|
||||
落点预检与其它付费编辑器生成共用同一条路径:提交时调只读 `preflight_editor_generation_target_and_return`,按认证 owner 对给出的落点逐个定点归属校验(两个都给就两个都查,不再为了一个目录 ID 读整个素材库),跨 owner / 已删除 / 不存在收敛成同一句 400。
|
||||
|
||||
**归一在预检之前完成并写回请求**:项目 ID 与素材夹 ID 都 trim,`project` / 旧 `folder-*` 走与图片画布同一个 `normalize_generated_asset_folder_id` 映射到当前 owner 的默认素材夹,素材名走同一个 `resolve_editor_generated_asset_label`(trim + 截断 + 缺省「3D 模型」)。入队的就是归一后的请求,因此预检值 = 队列载荷 = 落库值;模块侧落库原有的 `normalize_editor_generation_default_asset_folders` 继续兜底老队列载荷里的 `project`。
|
||||
|
||||
**定价相关参数在 API 层必填并做组合校验**:`texture`、`textureQuality`、`geometryQuality`、`quad`、`smartLowPoly`、`generateParts` 必须显式给出。`texture=false` 时禁止出现 `textureQuality`,且 `pbr` 必须显式 `false`;`texture=true` 时 `textureQuality` 必填。理由是 provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。
|
||||
|
||||
@@ -92,7 +94,7 @@ ImageToModelResult = { modelArtifact, renderedPreview }
|
||||
|
||||
每个 artifact 只携带正式资源引用与对象元数据,不携带 provider 临时 URL。
|
||||
|
||||
**完成结果的读取**:与其它画布生成任务同形——结果落在画布 / 素材行,客户端靠读回拿到。项目资源落点的 worker 写画布 placement(预览图层),客户端在任务终态后用 `loadEditorProject` 复读项目快照,按 `layer.objectKey` 打开模型;素材库落点写素材行。标准消费者的队列结果只回来源身份,**不透传 `result`**,因此 `/api/runtime/external-generation/jobs/{jobId}` 的 `result` 对 3D 为空——图片等其它画布生成任务也是如此。上面那份严格 tagged enum 是 worker 落库时构造的内部结果值:它只携带正式资源引用与对象元数据,不携带 provider 事实。
|
||||
**完成结果的读取**:与其它画布生成任务同形——结果落在画布 / 素材行,客户端靠读回拿到。项目资源落点的 worker 写画布 placement(预览图层),客户端在任务终态后用 `loadEditorProject` 复读项目快照,按 `layer.objectKey` 打开模型;素材库落点写素材行(画布链路两个落点都发,所以一次 3D 生成通常两条都写)。结果里的落点引用与请求落点同形:`target` 不再是被判别结构包着的分支,而是平坦的 `resourceId` / `assetId`,哪个落点写了行就带哪个 ID。前端画布链路的结果消费沿用既有画布路径(项目快照 + `applyGeneratedProjectSnapshot` 顺带刷新素材库),素材库行的 3D 缩略图按预览图渲染、不拿模型对象换签名地址。标准消费者的队列结果只回来源身份,**不透传 `result`**,因此 `/api/runtime/external-generation/jobs/{jobId}` 的 `result` 对 3D 为空——图片等其它画布生成任务也是如此。上面那份严格 tagged enum 是 worker 落库时构造的内部结果值:它只携带正式资源引用与对象元数据,不携带 provider 事实。
|
||||
|
||||
## 定价与扣费
|
||||
|
||||
@@ -159,7 +161,7 @@ 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 正文),不直接采信 provider 返回或下载响应头声明的类型;对象键扩展名由该内容类型派生。客户端不复制这份格式清单:画布只按「`assetKind=model3d` 且 `objectKey` 非空」放行,能否交给 3D 查看器由 `packages/model3d-viewer` 判定,判据依次是读接口声明的 `Content-Type`、字节魔数、地址扩展名,全判不出来即按 `unsupported-format` 报错。
|
||||
模型格式与体积由 `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 返回或下载响应头声明的类型,对象键扩展名由该内容类型派生。只有字节判不出来时才回落到声明值(预览最终回落 `image/webp`)。客户端不复制这份格式清单:画布只按「`assetKind=model3d` 且 `objectKey` 非空」放行,能否交给 3D 查看器由 `packages/model3d-viewer` 判定,判据依次是读接口声明的 `Content-Type`、字节魔数、地址扩展名,全判不出来即按 `unsupported-format` 报错。
|
||||
|
||||
`external_generation_job.phase` 的取值集合不变,仍只允许现有两种执行阶段;阶段文案由 api-server 映射,不扩展 schema 常量。
|
||||
|
||||
@@ -176,9 +178,9 @@ width/height = 预览图像素尺寸
|
||||
| 用户标签覆盖资格 | 允许,但只在 `model3d` 与空类别之间成立:3D 资源自成一族媒体语义,图片族不能覆盖成 3D,反之亦然 |
|
||||
| 快速编辑资格 | 不允许。3D 结果没有图片快速编辑语义,不进入快速编辑正向白名单;画布上「3D 预览」是只读查看能力,不是快速编辑 |
|
||||
| “改造” capability 与 V2 配方 | 不进入改造 allowlist,不定义 V2 `action` / `fields[].id` / `references[].id`,不发 `canvasCompletion` 之外的可重放配方;画布对 `model3d` 关闭改造入口 |
|
||||
| 素材库 / 后台 / 精选 read model / 公开 grant / External OpenAPI | 本期不改。素材库与后台沿用现有按 `assetKind` 派生 renderer 的行为,`model3d` 未接 renderer 前只保证字段正确,不承诺展示;公开 grant 与 External v1 均不涉及 |
|
||||
| 素材库 / 后台 / 精选 read model / 公开 grant / External OpenAPI | 素材库行已按 `assetKind` 派生 renderer 的口径接上 `model3d` 分支(缩略图走预览图,模型本体不进图片入口);后台列表、精选 read model、公开 grant 与 External v1 仍不涉及,只保证字段正确 |
|
||||
|
||||
清单第 3 条要求的“用户标签覆盖”与“快速编辑”两项资格评估互不推导:标签覆盖进入(限 `model3d` 与空类别互转),快速编辑与改造均不进入,理由是 3D 资源没有像素级编辑语义。画布入口只新增只读的「3D 预览」capability 位,由宿主按 `supportedActions` 声明可用;素材库行、后台 renderer 与精选 read model 仍未接,后续进入时必须单独补齐 renderer 与验收。
|
||||
清单第 3 条要求的“用户标签覆盖”与“快速编辑”两项资格评估互不推导:标签覆盖进入(限 `model3d` 与空类别互转),快速编辑与改造均不进入,理由是 3D 资源没有像素级编辑语义。画布入口只新增只读的「3D 预览」capability 位,由宿主按 `supportedActions` 声明可用;素材库行已接预览渲染与拖入画布,后台 renderer 与精选 read model 仍未接,后续进入时必须单独补齐 renderer 与验收。
|
||||
|
||||
## 兼容与迁移
|
||||
|
||||
@@ -197,7 +199,7 @@ width/height = 预览图像素尺寸
|
||||
7. submit 成功但 checkpoint 写入失败的 attempt 终态失败且不自动重提。
|
||||
8. provider 成功但落库失败时 job 失败、该 attempt 退款,checkpoint 保留供对账。
|
||||
9. worker 落库时构造的完成结果按端点严格类型化,只含模型与预览的正式资源引用,不含 provider task ID 与带签名的临时 URL;标准消费者的队列结果不透传它(与其它画布生成任务一致,结果靠画布 / 素材读回)。
|
||||
10. 素材库落点产出 `assetId`;项目资源落点产出 `resourceId`,且 `assetKind=model3d`、`AssetObject.content_type` 为按字节识别出的模型 mime、`content_length` 与实际对象一致。
|
||||
10. 素材库落点产出 `assetId`;项目资源落点产出 `resourceId`,且 `assetKind=model3d`、模型与预览的 `AssetObject.content_type` 都是按字节识别出的 mime(预览不再一律写成 `image/webp`)、`content_length` 与实际对象一致。
|
||||
11. 客户端媒体投影对 `model3d` 只暴露预览图:`imageSrc` / `thumbnailSrc` 指向预览对象、`objectKey` 指向模型本体;预览缺失时才回落到模型对象,且该回落不得让图片入口加载模型文件。
|
||||
12. 格式判定的顺序在查看器包内可被用例钉住:声明 `Content-Type` 优先于字节魔数、字节魔数优先于地址扩展名;三者都判不出来时模态展示 `unsupported-format` 原因与支持的格式清单,画布侧不做任何格式判断、也不在 `assetKind` 为空时按扩展名兜底。
|
||||
13. 真实 Provider smoke 覆盖 text-to-model 与 image-to-model 各一次,下载字节数与 `Content-Length` 一致。
|
||||
@@ -207,4 +209,4 @@ width/height = 预览图像素尺寸
|
||||
|
||||
- 加价项已按端点各持一份价目(当前数值相同);若 Tripo 后续对某个模型版本差异化加价,再补“按模型版本覆盖”一层,现在不预留。
|
||||
- 3D 模型的多文件 glTF 兄弟资源(`.bin`、贴图)、多视角与模型信息展示仍未安排;画布侧接入见 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](../project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md)。
|
||||
- AGC 项目画布自带两份格式判断(`resourceModelScene.ts` 的 `isResourceModelFbx` 把 `octet-stream` 当 FBX、`resourceCardPreviewModel.ts` 的资源卡预览按扩展名正则选解析器),本期不动,后续统一收敛到 `packages/model3d-viewer`;素材库行、后台 renderer、精选 read model 与公开 grant 的 3D 展示同样未进入。
|
||||
- AGC 项目画布自带两份格式判断(`resourceModelScene.ts` 的 `isResourceModelFbx` 把 `octet-stream` 当 FBX、`resourceCardPreviewModel.ts` 的资源卡预览按扩展名正则选解析器),本期不动,后续统一收敛到 `packages/model3d-viewer`;后台 renderer、精选 read model 与公开 grant 的 3D 展示同样未进入(素材库行只做预览图渲染与拖入画布,行内不开 3D 查看器)。
|
||||
|
||||
@@ -210,15 +210,43 @@ fn sniff_model_content_type(bytes: &[u8]) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// 产物 content type:模型先嗅探字节,嗅不出来才信 provider 声明。
|
||||
/// 预览图按字节魔数识别格式。识别范围就是线上真正会拿到的三种渲染图格式,其余返回 `None`
|
||||
/// 走声明值回落 —— 不为了少见的格式去猜一个可能同样错的类型。
|
||||
fn sniff_preview_content_type(bytes: &[u8]) -> Option<&'static str> {
|
||||
let format = image::ImageReader::new(std::io::Cursor::new(bytes))
|
||||
.with_guessed_format()
|
||||
.ok()?
|
||||
.format()?;
|
||||
match format {
|
||||
image::ImageFormat::Png => Some("image/png"),
|
||||
image::ImageFormat::Jpeg => Some("image/jpeg"),
|
||||
image::ImageFormat::WebP => Some("image/webp"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 产物 content type:两个槽位都先嗅探字节,嗅不出来才信 provider 声明。
|
||||
///
|
||||
/// provider 对二进制产物常给 `application/octet-stream`,归一后会被写成
|
||||
/// `model/gltf-binary` + `.glb`;客户端按这份 content type 判定能不能预览,说错就是
|
||||
/// 拿 GLTFLoader 去解 FBX。预览图不嗅探:它已经有维度解析这条更硬的正向校验。
|
||||
/// 拿 GLTFLoader 去解 FBX。预览图同理:provider / CDN 常把 PNG 渲染图标成
|
||||
/// `application/octet-stream` 或干脆不写 `Content-Type`,照抄声明就会把 `image/webp`
|
||||
/// 写进 OSS metadata 与 `asset_object.content_type`,素材库下载命名与后台等非浏览器消费方
|
||||
/// 都会拿到错的类型。浏览器 `<img>` 自己能按魔数嗅探,但那不是让记录值继续说谎的理由。
|
||||
///
|
||||
/// 这里不把 worker 的 `preview_dimensions` 猜出的格式传下来:那一步是「必须能解码」的正交校验,
|
||||
/// 与写库时的类型归一各管一件事,重复的只是读文件头这一下,换来判定点只有一个。
|
||||
fn resolve_artifact_content_type(raw: &str, slot: Model3dArtifactSlot, bytes: &[u8]) -> String {
|
||||
if slot == Model3dArtifactSlot::Model {
|
||||
if let Some(sniffed) = sniff_model_content_type(bytes) {
|
||||
return sniffed.to_string();
|
||||
match slot {
|
||||
Model3dArtifactSlot::Model => {
|
||||
if let Some(sniffed) = sniff_model_content_type(bytes) {
|
||||
return sniffed.to_string();
|
||||
}
|
||||
}
|
||||
Model3dArtifactSlot::Preview => {
|
||||
if let Some(sniffed) = sniff_preview_content_type(bytes) {
|
||||
return sniffed.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
normalize_artifact_content_type(raw, slot)
|
||||
@@ -250,6 +278,9 @@ mod tests {
|
||||
const FBX_BYTES: &[u8] = b"Kaydara FBX Binary \x00\x1a\x00rest-of-fbx";
|
||||
const GLTF_JSON_BYTES: &[u8] = b"\xEF\xBB\xBF\n {\"asset\":{\"version\":\"2.0\"}}";
|
||||
const UNKNOWN_BYTES: &[u8] = b"\x00\x01not-a-model";
|
||||
const PNG_BYTES: &[u8] = b"\x89PNG\r\n\x1a\nrest-of-png";
|
||||
const JPEG_BYTES: &[u8] = b"\xFF\xD8\xFF\xE0rest-of-jpeg";
|
||||
const WEBP_BYTES: &[u8] = b"RIFF\x24\x00\x00\x00WEBPVP8 rest-of-webp";
|
||||
|
||||
#[test]
|
||||
fn model_content_type_follows_bytes_over_declaration() {
|
||||
@@ -331,8 +362,49 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_content_type_is_not_sniffed() {
|
||||
// 预览图不按模型魔数嗅探:即使字节恰好长得像模型,也走图片的既有归一。
|
||||
fn preview_content_type_follows_bytes_over_declaration() {
|
||||
// Tripo 的渲染图常带 application/octet-stream(或被 CDN 标错):照抄声明会把 PNG
|
||||
// 写成 .webp,类型与扩展名一起错到 asset_object.content_type 与素材库下载命名上。
|
||||
for (bytes, declared, expected) in [
|
||||
(PNG_BYTES, "application/octet-stream", "image/png"),
|
||||
(PNG_BYTES, "", "image/png"),
|
||||
(PNG_BYTES, "image/jpeg", "image/png"),
|
||||
(JPEG_BYTES, "application/octet-stream", "image/jpeg"),
|
||||
(WEBP_BYTES, "application/octet-stream", "image/webp"),
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_artifact_content_type(declared, Model3dArtifactSlot::Preview, bytes),
|
||||
expected,
|
||||
"声明 {declared} 的预览图必须按字节判定"
|
||||
);
|
||||
}
|
||||
// 类型与对象键扩展名同源于这一次判定:PNG 预览落库是 preview.png。
|
||||
assert_eq!(
|
||||
artifact_extension("image/png", Model3dArtifactSlot::Preview),
|
||||
"png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_content_type_keeps_declaration_when_bytes_are_unknown() {
|
||||
// 嗅不出图片格式时保持既有行为:声明可用就信声明,否则回落 webp。
|
||||
assert_eq!(
|
||||
resolve_artifact_content_type(
|
||||
"image/jpeg; charset=binary",
|
||||
Model3dArtifactSlot::Preview,
|
||||
UNKNOWN_BYTES,
|
||||
),
|
||||
"image/jpeg"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_artifact_content_type(
|
||||
"application/octet-stream",
|
||||
Model3dArtifactSlot::Preview,
|
||||
UNKNOWN_BYTES,
|
||||
),
|
||||
"image/webp"
|
||||
);
|
||||
// 模型字节不是预览图格式,不参与预览判定,仍走图片归一。
|
||||
assert_eq!(
|
||||
resolve_artifact_content_type(
|
||||
"application/octet-stream",
|
||||
@@ -341,14 +413,6 @@ mod tests {
|
||||
),
|
||||
"image/webp"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_artifact_content_type(
|
||||
"image/jpeg; charset=binary",
|
||||
Model3dArtifactSlot::Preview,
|
||||
GLB_BYTES,
|
||||
),
|
||||
"image/jpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user