统一普通图片上传素材类型语义

修复普通图片类型迁移漏改上传工作流断言的问题。

普通静态图片的项目资源与账号素材创建请求统一省略 assetKind。

补充上传回归断言并同步图片画布上传契约文档。
This commit is contained in:
2026-08-09 16:17:15 +08:00
parent ba87f24c75
commit e0008b11a6
4 changed files with 14 additions and 5 deletions
@@ -210,7 +210,7 @@
- 快速编辑 / 重绘站内 public 示例图、历史 generated 图或 OSS generated 图时,优先复用当前图层已有 `objectKey` / `resourceId` / `sourceAssetId`;尚未登记且没有稳定引用的浏览器本地图片或普通 public 图片路径都必须先上传并取得 objectKey。前端不得再把正式对象下载成 `data:image/*;base64,...` 后提交,也不得把 Data URL / Blob URL 写入外部生成持久任务 JSON;后端收到引用后统一做 owner 归属校验并签名读取。
- 快速编辑不保留用户可添加的额外参考图入口;点击修改时把原图已登记业务 ID 作为 `/api/editor/images/edits``sourceReferenceId`,红框序号标注图上传后作为辅助 `referenceImageSrcs`
- 素材文件夹可以新建、折叠、重命名和删除;删除普通文件夹后,其素材移动到“项目素材”。普通上传默认落入“上传素材”文件夹;素材库缺少该文件夹时,前端在首次普通上传前创建一次并复用,拖到指定文件夹或点击指定文件夹上传时仍进入目标文件夹。
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;上传视频同样在创建素材和图层前读取视频 metadata 宽高,保证单层下载或 ZIP 导出的真实视频文件重新导入后仍按文件自身尺寸入画布;仅在无法解析尺寸时才使用对应媒体兜底尺寸。OSS 直传确认成功后,即时占位素材、即时画布图层和补充图片尺寸探测必须使用本次换签得到的 signed URL,不得把私有对象的裸 `/generated-*` 兼容路径交给浏览器读取;账号素材持久化仍只写 `legacyPublicPath``objectKey``assetObjectId`,禁止把会过期的 signed URL 写入资源或素材记录。
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;上传视频同样在创建素材和图层前读取视频 metadata 宽高,保证单层下载或 ZIP 导出的真实视频文件重新导入后仍按文件自身尺寸入画布;仅在无法解析尺寸时才使用对应媒体兜底尺寸。普通静态图片上传创建项目资源或账号素材时必须省略 `assetKind`,持久化为 `null/None``"image"` 只是前端 `CanvasMediaType`,不是 `CanvasAssetKind`OSS 直传确认成功后,即时占位素材、即时画布图层和补充图片尺寸探测必须使用本次换签得到的 signed URL,不得把私有对象的裸 `/generated-*` 兼容路径交给浏览器读取;账号素材持久化仍只写 `legacyPublicPath``objectKey``assetObjectId`,禁止把会过期的 signed URL 写入资源或素材记录。
- 音频 / 视频素材卡和画布媒体图层必须提供稳定的非文字视觉预览:优先使用 `thumbnailSrc` / 视频 `poster`,没有真实首帧或音频封面时使用由媒体类型、素材名和地址派生的确定性视觉底图。视频图层使用原生 `<video controls preload="metadata" playsInline>` 播放,外层图层仍承接选择和拖拽语义;音频图层播放前继续通过 `/api/assets/read-url` 换签。画布素材导出按 `mediaType` 保留真实媒体格式:图片进入 `images/`,音频 / 视频进入 `media/`,文件扩展名从响应 MIME、`objectKey` 或源 URL 推断,不得把音频 / 视频导出成 PNG。
- 画布素材 ZIP 的 `metadata.json` 只保存前端信息弹窗和导出文件列表可见的展示快照:项目标题、导出时间、图层标题、文件路径、类型、生成输入、模型显示名、Task 短 ID、Object 显示值、Resolution / 时长和导出错误。导出的生成输入只保留用户实际填写或选择的内容;系统默认兜底提示词、固定工作流提示词、内置图标描述、UI 提取素材固定提示词等内置提示词即使存在于历史 `generationInputs`,也不得写入导出元数据。不得把 `projectId``layerId``resourceId``sourceAssetId``sourceResourceId`、原始 `prompt` / `actualPrompt` / `provider` 或画布坐标、锁定、隐藏等布局状态写入导出元数据;Object 字段仅沿用信息弹窗当前可见值。
- `生成角色动作` 的完成结果按序列帧素材处理:`assetKind` 固定为 `character-animation`,图层主 `src` 使用首帧,前端内部据 `assetKind` 派生 `CanvasMediaType="image-sequence"` 选择播放器。`previewVideoPath` 只作为上游来源,不作为画布主媒体。下载和 ZIP 导出必须得到序列帧 ZIP / frames 目录,不能回退为预览视频或首帧 PNG;后端抽帧后逐帧保存带绿幕源图,上传时消费并释放原帧字节,再用该对象的 600 秒 OSS URL 完成透明化并落盘透明帧素材。
@@ -14,6 +14,8 @@ export function resolveUploadedAssetKind(
case 'image-sequence':
throw new Error('序列帧素材不支持按单个媒体文件上传');
case 'image':
// 普通静态图片没有 CanvasAssetKind;返回 undefined 让创建请求省略该字段,
// 服务端和 SpacetimeDB 按 None 持久化。
return undefined;
}
}
@@ -631,6 +631,9 @@ describe('useImageCanvasUploadWorkflow', () => {
assetObjectId: 'assetobj-editor-image',
}),
);
expect(createEditorAssetMock.mock.calls[0]?.[0]).not.toHaveProperty(
'assetKind',
);
expect(measuredImageSources).toContain(
'https://signed.example.com/editor/image/画布素材.png',
);
@@ -784,9 +787,11 @@ describe('useImageCanvasUploadWorkflow', () => {
width: 420,
height: 315,
sourceType: 'uploaded',
assetKind: 'image',
}),
);
expect(
createEditorProjectResourceMock.mock.calls[0]?.[1],
).not.toHaveProperty('assetKind');
expect(createEditorAssetMock).not.toHaveBeenCalled();
});
@@ -349,7 +349,7 @@ export function useImageCanvasUploadWorkflow({
width,
height,
sourceType: 'uploaded',
assetKind,
...(assetKind !== undefined ? { assetKind } : {}),
generationInputs,
});
return createUploadedGenerationReference({
@@ -383,7 +383,7 @@ export function useImageCanvasUploadWorkflow({
width,
height,
sourceType: 'uploaded',
assetKind,
...(assetKind !== undefined ? { assetKind } : {}),
generationInputs,
});
return createUploadedGenerationReference({
@@ -1411,7 +1411,9 @@ export function useImageCanvasUploadWorkflow({
width: uploadedAsset.width,
height: uploadedAsset.height,
sourceType: 'uploaded',
assetKind: nextLayer.assetKind,
...(nextLayer.assetKind != null
? { assetKind: nextLayer.assetKind }
: {}),
generationInputs: nextLayer.generationInputs,
})
.then((asset) => {