删除普通图片素材类型兼容
移除 api-server 和 SpacetimeDB 对错误图片类型值的归一化与迁移逻辑。 恢复上传工作流直接传递可选 assetKind,并删除对应错误夹具与断言。 保留媒体类型与素材类型不得混用的文档约束,删除历史兼容说明。
This commit is contained in:
@@ -6614,7 +6614,6 @@
|
||||
- 2026-08-06 修订:画布 Agent 的 `edit_image` 只接受图片输入,新任务以 `assetKind=null` 表示普通静态图片,不再使用 synthetic `editor_agent_edit_image`。worker 仅按服务端生成的 `editor-agent:` dedupe namespace 识别并归一历史排队 payload;普通调用伪造旧值继续被拒绝。已持久化资源中的旧值只有在后端从真实目标图层 / 项目资源解析后才兼容为空类型,避免历史 Agent 结果失去快速编辑能力,同时不扩大请求白名单。
|
||||
- 2026-08-07 修订:站内与 External v1 图片编辑请求统一只接受必填 `sourceReferenceId`,且该值必须是当前账号已登记的项目资源 ID 或素材 ID;objectKey、URL、Data URL、Blob URL 以及旧 `sourceImageSrc/sourceResourceId/assetKind` 字段全部返回 400,不提供兼容别名。后端用共享窄查询分别按两张表主键定点解析,双表同 ID、未命中、跨账号、对象缺失或越权均失败关闭;权威类型完全来自业务记录,只允许普通静态图片、规范图、角色图、完整图标图集、图标规范、宣发图和 UI 设计图。请求带 `targetLayerId` 时必须同时带 `projectId`,来源与目标优先比较 `assetObjectId`,任一方缺失才比较 canonical `(bucket, objectKey)`,且来源默认类型必须与目标资源默认类型一致;最终类型取目标覆盖值或目标资源类型。HTTP 入队写入版本化服务端解析快照,worker 执行前按同一业务 ID 再次定点解析,身份或类型漂移即失败关闭。旧任务只把已有资源 ID 或旧来源字符串本身当业务 ID 迁移,绝不按 objectKey 反查。Canvas Agent 必须从 `ImageMetadata.reference_id` 取主来源;红框标注上传图只作为辅助 `referenceImageSrcs`,不能冒充被编辑资源。仅以素材 ID 编辑时,队列审计与 `generationInputs.references` 保留素材 ID,不伪造项目资源关系。
|
||||
- 2026-08-08 修订:`scene` 是单张静态图片素材,加入前端快速编辑正向白名单和 api-server 权威来源白名单;编辑结果继续保留 `scene`。用户标签覆盖侧的前端菜单与 SpacetimeDB 结构化布局白名单也必须显式覆盖 `scene`。通用图片生成接口仍拒绝 `scene`,避免绕过结构化场景生成契约。
|
||||
- 2026-08-08 修订:普通静态图片的正式语义值固定为 `assetKind=null/None`;`"image"` 只属于前端派生 `CanvasMediaType`、MIME / content type 和生成参考媒体类型,不是 `CanvasAssetKind`,不得由上传、生成、资源 / 素材写入或 read model 生产。API 与 SpacetimeDB storage 的读写边界把短期误写的历史 `assetKind="image"` 原位归一为空;快速编辑白名单继续直接允许空类型,不新增 `"image"` 分支。
|
||||
- 2026-08-08 修订:画布 Agent 的 `edit_image` 工具内部可先形成待确认的 `EditorImageEditRequest`,但确认接口必须在通用入队前复用站内图片编辑的来源解析与目标预检,写入 `{ version, request, source }` 服务端快照。worker 只解析正式 versioned payload 与既有历史 payload,不接受当前 direct request 作为 fallback;这样未上线的 Agent 路径在生产端原位修正,不扩大消费端协议。
|
||||
- 2026-08-08 修订:图片编辑业务引用按 `objectKey` 找到权威 `asset_object` 后,若资源或素材记录同时保存了 `assetObjectId`,必须验证两者指向同一对象;不一致时在 SpacetimeDB resolver 边界失败关闭,不能把未验证的记录 ID 与已验证的对象路径组合进 snapshot。缺少 ID 的历史记录继续以 canonical `(bucket, objectKey)` 作为对象身份。
|
||||
- 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单测试覆盖单个拆分图标、角色动作及音频不展示,提交工作流测试覆盖单个拆分图标和角色动作绕过入口时仍拒绝;后端表驱动测试覆盖全部现役素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。
|
||||
|
||||
@@ -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 导出的真实视频文件重新导入后仍按文件自身尺寸入画布;仅在无法解析尺寸时才使用对应媒体兜底尺寸。普通静态图片上传创建项目资源或账号素材时必须省略 `assetKind`,持久化为 `null/None`;`"image"` 只是前端 `CanvasMediaType`,不是 `CanvasAssetKind`。OSS 直传确认成功后,即时占位素材、即时画布图层和补充图片尺寸探测必须使用本次换签得到的 signed URL,不得把私有对象的裸 `/generated-*` 兼容路径交给浏览器读取;账号素材持久化仍只写 `legacyPublicPath`、`objectKey` 和 `assetObjectId`,禁止把会过期的 signed URL 写入资源或素材记录。
|
||||
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;上传视频同样在创建素材和图层前读取视频 metadata 宽高,保证单层下载或 ZIP 导出的真实视频文件重新导入后仍按文件自身尺寸入画布;仅在无法解析尺寸时才使用对应媒体兜底尺寸。普通静态图片的 `assetKind` 为空;`"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 完成透明化并落盘透明帧素材。
|
||||
|
||||
@@ -440,7 +440,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
|
||||
## 编辑器角色动作素材持久化契约(2026-07-28)
|
||||
|
||||
- `asset_kind` 是资源 / 素材可选的权威语义类别,不新增或返回并列的 `media_type` / `mediaType`。普通静态图片固定使用 `asset_kind = NULL`,`image` 只可作为前端派生媒体类型、MIME / content type 或生成参考的媒体类型,不能作为语义 `asset_kind`;API 与 SpacetimeDB storage 的写入和 snapshot 边界把历史误值 `image` 归一为 `None`。角色动作预览 MP4 使用 `asset_kind = video`,最终透明帧集使用 `asset_kind = character-animation`;前端据此派生具体渲染器。
|
||||
- `asset_kind` 是资源 / 素材可选的权威语义类别,不新增或返回并列的 `media_type` / `mediaType`。普通静态图片的 `asset_kind` 为空;角色动作预览 MP4 使用 `asset_kind = video`,最终透明帧集使用 `asset_kind = character-animation`;前端据此派生具体渲染器。
|
||||
- `editor_project_resource` 与 `editor_asset` 表尾只追加 `image_sequence_frames_json: Option<String>` 和 `image_sequence_duration_ms: Option<u64>`;`editor_showcase_asset` 作为提交时冻结的审核与公开快照,也在表尾追加并从账号素材复制相同两字段。前者保存完整有效帧数组,数组位置是唯一播放顺序,正式帧对象不保存或返回 `frameIndex`;后者只表示该图片序列完整播放一次的毫秒时长。帧数始终取数组长度,FPS 在播放或导出时即时推导,不持久化 `frame_count` 或 `fps`。
|
||||
- 图片序列时长不能复用音频 / 视频生成请求的 `durationSeconds`,资源和素材也不保存通用 `duration_seconds`。角色动作与视频生成响应保留各自既有的请求 / 结果级秒数;普通音频 / 视频的用户可见时长只作为字符串展示项写入 `generation_inputs_json.fields[]`,上传媒体使用本次上传探测值,带时长选项的生成任务使用用户提交值,不再复制到 `EditorAsset`、`CanvasLayer` 或画布 layout,也不在素材放置 / 工程恢复时探测或从 layout、resource 做双来源回退。素材详情和画布 ZIP 用户可见元数据只透传实际存在的 `fields[]` 时长项;缺少该项时省略时长,不生成 `--:--` 等占位值。音频播放控件只信任 `<audio>` 的 `loadedmetadata.duration`,展示字段不得参与播放、裁切或变速。SFX V2 是唯一例外:完成响应的 `durationSeconds` 必须来自 MP3 探测,且同一实际值写入 `generation_inputs_json.soundEffect.actualDurationSeconds`;该例外仍不得新增资源 / 素材正式时长列、写入 `EditorAsset` / `CanvasLayer` / layout,或复用图片序列字段。
|
||||
- 角色动作 worker 在透明帧全部持久化后创建最终项目资源与账号素材,并写入帧数组与 `image_sequence_duration_ms`;生成响应可继续返回请求 / 结果层面的 `frameCount`、`fps`、`durationSeconds`,但它们不是持久化真相。预览视频仍作为独立 `asset_kind = video` 中间素材保留并承担生成成本,最终派生素材成本为 0。
|
||||
@@ -477,7 +477,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
|
||||
其它注意事项:
|
||||
|
||||
- `assetKind` 是数据库、Rust DTO 和对外 JSON 的可选语义类别真相;普通静态图片必须为空,不能新增 `image` 类别。不要新增或返回并列的 `mediaType`;前端 renderer 可以保留内部派生媒体类型,但不得回写后端。
|
||||
- `assetKind` 是数据库、Rust DTO 和对外 JSON 的可选语义类别真相;普通静态图片必须为空。不要新增或返回并列的 `mediaType`;前端 renderer 可以保留内部派生媒体类型,但不得回写后端。
|
||||
- 每次新增 `assetKind` 都必须分别完成“用户标签覆盖”和“快速编辑”两项资格评估;两者互不推导。可被用户选择不等于可快速编辑,属于图片媒体族也不等于自动进入快速编辑正向白名单。
|
||||
- `assetKind` 只描述素材类别,不代表生成配方可执行或允许“改造”;新增类别必须单独完成上表的 capability 决策和验收。
|
||||
- 只是新增分类或 renderer 且现有媒体字段足够时,不改 schema。只有必须跨刷新、复用、审核或公开保留的数据才新增类别专属字段。
|
||||
|
||||
@@ -182,7 +182,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn project_with_resource_asset_kind(asset_kind: &str) -> EditorProjectRecord {
|
||||
fn project_with_resource_asset_kind(asset_kind: Option<&str>) -> EditorProjectRecord {
|
||||
let viewport = EditorCanvasViewportRecord {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
provider: None,
|
||||
task_id: None,
|
||||
source_resource_id: None,
|
||||
asset_kind: Some(asset_kind.to_string()),
|
||||
asset_kind: asset_kind.map(str::to_string),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: false,
|
||||
created_at: "2026-08-08T00:00:00Z".to_string(),
|
||||
@@ -271,7 +271,7 @@ mod tests {
|
||||
"icon_descriptions": ["背包"]
|
||||
});
|
||||
|
||||
let ordinary_project = project_with_resource_asset_kind("image");
|
||||
let ordinary_project = project_with_resource_asset_kind(None);
|
||||
let ordinary_context =
|
||||
build_tool_context_from_authoritative_records(&document, Some(&ordinary_project), None);
|
||||
let ordinary_tool = editor_agent_tool(GenerateIconSpritesheetTool::NAME, &ordinary_context)
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
.expect_err("ordinary registered image must be rejected before confirmation");
|
||||
assert!(error.to_string().contains("icon-spec"));
|
||||
|
||||
let icon_spec_project = project_with_resource_asset_kind("icon-spec");
|
||||
let icon_spec_project = project_with_resource_asset_kind(Some("icon-spec"));
|
||||
let icon_spec_context = build_tool_context_from_authoritative_records(
|
||||
&document,
|
||||
Some(&icon_spec_project),
|
||||
|
||||
@@ -2120,9 +2120,8 @@ pub async fn create_editor_project_resource(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorProjectResourceCreateRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let asset_kind = normalize_editor_semantic_asset_kind(payload.asset_kind);
|
||||
let generation_inputs_json = serialize_editor_generation_inputs(
|
||||
asset_kind.as_deref(),
|
||||
payload.asset_kind.as_deref(),
|
||||
sanitize_editor_untrusted_generation_inputs(payload.generation_inputs.clone()),
|
||||
)?;
|
||||
let image_sequence_frames_json =
|
||||
@@ -2147,7 +2146,7 @@ pub async fn create_editor_project_resource(
|
||||
provider: normalize_optional_string(payload.provider),
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
source_resource_id: normalize_optional_string(payload.source_resource_id),
|
||||
asset_kind,
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
generation_inputs_json,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
image_sequence_frames_json,
|
||||
@@ -2295,9 +2294,8 @@ pub async fn create_editor_asset(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorAssetCreateRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let asset_kind = normalize_editor_semantic_asset_kind(payload.asset_kind);
|
||||
let generation_inputs_json = serialize_editor_generation_inputs(
|
||||
asset_kind.as_deref(),
|
||||
payload.asset_kind.as_deref(),
|
||||
sanitize_editor_untrusted_generation_inputs(payload.generation_inputs.clone()),
|
||||
)?;
|
||||
let image_sequence_frames_json =
|
||||
@@ -2322,7 +2320,7 @@ pub async fn create_editor_asset(
|
||||
model: normalize_optional_string(payload.model),
|
||||
provider: normalize_optional_string(payload.provider),
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
asset_kind,
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
generation_inputs_json,
|
||||
source_resource_id: None,
|
||||
now_micros: current_utc_micros(),
|
||||
@@ -4080,7 +4078,10 @@ struct EditorImageEditTargetLayerSource {
|
||||
fn normalize_editor_image_edit_resolved_source_kind(
|
||||
resolved_asset_kind: Option<&str>,
|
||||
) -> Option<String> {
|
||||
normalize_editor_semantic_asset_kind(resolved_asset_kind.map(ToOwned::to_owned))
|
||||
resolved_asset_kind
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn resolve_editor_image_edit_target_layer_source(
|
||||
@@ -5043,13 +5044,21 @@ fn resolve_editor_pixel_art_snap_asset_kind(
|
||||
discovered_source_asset_kinds: &[String],
|
||||
storage_asset_kinds: &[String],
|
||||
) -> Result<Option<String>, AppError> {
|
||||
let requested_asset_kind =
|
||||
normalize_editor_semantic_asset_kind(requested_asset_kind.map(ToOwned::to_owned));
|
||||
let preferred_source_asset_kind =
|
||||
normalize_editor_semantic_asset_kind(source_asset_kind.flatten().map(ToOwned::to_owned));
|
||||
let requested_asset_kind = requested_asset_kind
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
let preferred_source_asset_kind = source_asset_kind
|
||||
.flatten()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
let discovered_source_asset_kinds = discovered_source_asset_kinds
|
||||
.iter()
|
||||
.filter_map(|value| normalize_editor_semantic_asset_kind(Some(value.clone())))
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
let storage_asset_kinds = storage_asset_kinds
|
||||
.iter()
|
||||
@@ -9082,8 +9091,8 @@ pub(crate) fn resolve_editor_asset_kind(
|
||||
image_src: &str,
|
||||
object_key: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if let Some(asset_kind) = normalize_editor_semantic_asset_kind(asset_kind) {
|
||||
return Some(asset_kind);
|
||||
if asset_kind.is_some() {
|
||||
return asset_kind;
|
||||
}
|
||||
let source_is_video =
|
||||
editor_asset_source_has_extension(image_src, object_key, &["mp4", "mov", "webm", "avi"]);
|
||||
@@ -11128,10 +11137,6 @@ pub(crate) fn normalize_optional_string(value: Option<String>) -> Option<String>
|
||||
.filter(|item| !item.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_editor_semantic_asset_kind(value: Option<String>) -> Option<String> {
|
||||
normalize_optional_string(value).filter(|asset_kind| asset_kind != "image")
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_editor_generated_asset_label(
|
||||
value: Option<String>,
|
||||
fallback: &str,
|
||||
@@ -14888,7 +14893,7 @@ mod tests {
|
||||
.expect_err("legacy animation runtime metadata must fail explicitly");
|
||||
assert!(error.body_text().contains("imageSequenceFrames"));
|
||||
assert!(error.body_text().contains("imageSequenceDurationMs"));
|
||||
assert!(serialize_editor_generation_inputs(Some("image"), Some(metadata)).is_ok());
|
||||
assert!(serialize_editor_generation_inputs(None, Some(metadata)).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15593,10 +15598,6 @@ mod tests {
|
||||
omitted_storage_kind_error.status_code(),
|
||||
StatusCode::BAD_REQUEST
|
||||
);
|
||||
|
||||
let image_kind = resolve_editor_pixel_art_snap_asset_kind(Some("image"), None, &[], &[])
|
||||
.expect("legacy ordinary-image kind should normalize to none");
|
||||
assert_eq!(image_kind, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -19211,11 +19212,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_editor_agent_resource_kind_fails_closed() {
|
||||
assert_eq!(
|
||||
normalize_editor_image_edit_resolved_source_kind(Some("image")),
|
||||
None
|
||||
);
|
||||
fn synthetic_editor_agent_resource_kind_fails_closed() {
|
||||
let resolved_kind =
|
||||
normalize_editor_image_edit_resolved_source_kind(Some("editor_agent_edit_image"));
|
||||
assert_eq!(resolved_kind.as_deref(), Some("editor_agent_edit_image"));
|
||||
|
||||
@@ -2588,7 +2588,7 @@ fn prepare_editor_project_resource(
|
||||
let model = normalize_optional(input.model);
|
||||
let provider = normalize_optional(input.provider);
|
||||
let task_id = normalize_optional(input.task_id);
|
||||
let asset_kind = normalize_editor_semantic_asset_kind(input.asset_kind);
|
||||
let asset_kind = normalize_optional(input.asset_kind);
|
||||
let generation_inputs_json = normalize_optional(input.generation_inputs_json);
|
||||
let image_sequence_frames_json = normalize_optional(input.image_sequence_frames_json);
|
||||
let image_sequence_duration_ms = input.image_sequence_duration_ms;
|
||||
@@ -2669,7 +2669,7 @@ fn repair_editor_project_resource_media(
|
||||
source_resource_id: resource.source_resource_id,
|
||||
created_at: resource.created_at,
|
||||
updated_at: now,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(resource.asset_kind),
|
||||
asset_kind: resource.asset_kind,
|
||||
generation_inputs_json: resource.generation_inputs_json,
|
||||
public_showcase_enabled: resource.public_showcase_enabled,
|
||||
image_sequence_frames_json: resource.image_sequence_frames_json,
|
||||
@@ -4390,9 +4390,8 @@ fn validate_editor_pixel_art_result_input(
|
||||
.ok_or_else(|| "完美像素 asset_object 缺少稳定 task_id".to_string())?;
|
||||
let resource_task_id = normalize_optional(input.project_resource.task_id.clone());
|
||||
let asset_task_id = normalize_optional(input.asset.task_id.clone());
|
||||
let resource_asset_kind =
|
||||
normalize_editor_semantic_asset_kind(input.project_resource.asset_kind.clone());
|
||||
let asset_asset_kind = normalize_editor_semantic_asset_kind(input.asset.asset_kind.clone());
|
||||
let resource_asset_kind = normalize_optional(input.project_resource.asset_kind.clone());
|
||||
let asset_asset_kind = normalize_optional(input.asset.asset_kind.clone());
|
||||
let expected_task_id = format!("pixel-art-snap-{operation_id}");
|
||||
let expected_asset_object_id = editor_pixel_art_stable_record_id(
|
||||
owner_user_id,
|
||||
@@ -4694,7 +4693,7 @@ fn editor_pixel_art_project_resource_matches(
|
||||
&& existing.provider == normalize_optional(input.provider.clone())
|
||||
&& existing.task_id == normalize_optional(input.task_id.clone())
|
||||
&& existing.source_resource_id == normalize_optional(input.source_resource_id.clone())
|
||||
&& existing.asset_kind == normalize_editor_semantic_asset_kind(input.asset_kind.clone())
|
||||
&& existing.asset_kind == normalize_optional(input.asset_kind.clone())
|
||||
&& existing.generation_inputs_json
|
||||
== normalize_optional(input.generation_inputs_json.clone())
|
||||
&& !existing.public_showcase_enabled
|
||||
@@ -4716,7 +4715,7 @@ fn editor_pixel_art_asset_matches(existing: &EditorAsset, input: &EditorAssetCre
|
||||
&& existing.model == normalize_optional(input.model.clone())
|
||||
&& existing.provider == normalize_optional(input.provider.clone())
|
||||
&& existing.task_id == normalize_optional(input.task_id.clone())
|
||||
&& existing.asset_kind == normalize_editor_semantic_asset_kind(input.asset_kind.clone())
|
||||
&& existing.asset_kind == normalize_optional(input.asset_kind.clone())
|
||||
&& existing.generation_inputs_json
|
||||
== normalize_optional(input.generation_inputs_json.clone())
|
||||
&& existing.source_resource_id == normalize_optional(input.source_resource_id.clone())
|
||||
@@ -5036,7 +5035,7 @@ fn editor_spritesheet_asset_matches_create_input(
|
||||
&& existing.model == normalize_optional(input.model.clone())
|
||||
&& existing.provider == normalize_optional(input.provider.clone())
|
||||
&& existing.task_id == normalize_optional(input.task_id.clone())
|
||||
&& existing.asset_kind == normalize_editor_semantic_asset_kind(input.asset_kind.clone())
|
||||
&& existing.asset_kind == normalize_optional(input.asset_kind.clone())
|
||||
&& existing.generation_inputs_json
|
||||
== normalize_optional(input.generation_inputs_json.clone())
|
||||
&& existing.source_resource_id == normalize_optional(input.source_resource_id.clone())
|
||||
@@ -5427,7 +5426,7 @@ fn create_editor_asset(
|
||||
if group_task_id.is_some() {
|
||||
require_editor_generation_runtime_service_identity(ctx, caller)?;
|
||||
}
|
||||
let asset_kind = normalize_editor_semantic_asset_kind(input.asset_kind);
|
||||
let asset_kind = normalize_optional(input.asset_kind);
|
||||
let generation_inputs_json = normalize_optional(input.generation_inputs_json);
|
||||
let image_sequence_frames_json = normalize_optional(input.image_sequence_frames_json);
|
||||
validate_editor_generation_inputs(asset_kind.as_deref(), generation_inputs_json.as_deref())?;
|
||||
@@ -5510,7 +5509,7 @@ fn update_editor_asset(
|
||||
task_id: asset.task_id,
|
||||
created_at: asset.created_at,
|
||||
updated_at: now,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(asset.asset_kind),
|
||||
asset_kind: asset.asset_kind,
|
||||
generation_inputs_json: asset.generation_inputs_json,
|
||||
source_resource_id: asset.source_resource_id,
|
||||
thumbnail_src: asset.thumbnail_src,
|
||||
@@ -5560,7 +5559,7 @@ fn repair_editor_asset_media(
|
||||
task_id: asset.task_id,
|
||||
created_at: asset.created_at,
|
||||
updated_at: now,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(asset.asset_kind),
|
||||
asset_kind: asset.asset_kind,
|
||||
generation_inputs_json: asset.generation_inputs_json,
|
||||
source_resource_id: asset.source_resource_id,
|
||||
thumbnail_src: asset.thumbnail_src,
|
||||
@@ -5684,7 +5683,7 @@ fn build_pending_editor_showcase_asset(
|
||||
provider: asset.provider,
|
||||
task_id: asset.task_id,
|
||||
source_resource_id: asset.source_resource_id,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(asset.asset_kind),
|
||||
asset_kind: asset.asset_kind,
|
||||
generation_inputs_json: asset.generation_inputs_json,
|
||||
thumbnail_src: asset.thumbnail_src,
|
||||
generation_cost_mud_points: asset.generation_cost_mud_points,
|
||||
@@ -7244,7 +7243,7 @@ fn normalize_legacy_editor_source_asset_kind(
|
||||
object_key: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if asset_kind.as_deref() != Some(EDITOR_LEGACY_GREEN_SCREEN_SOURCE_ASSET_KIND) {
|
||||
return normalize_editor_semantic_asset_kind(asset_kind);
|
||||
return asset_kind;
|
||||
}
|
||||
let object_key = object_key.unwrap_or_default().to_ascii_lowercase();
|
||||
if object_key.contains("/character-images/") {
|
||||
@@ -7362,7 +7361,7 @@ fn showcase_snapshot_from_row(row: EditorShowcaseAsset) -> EditorShowcaseAssetSn
|
||||
provider: row.provider,
|
||||
task_id: row.task_id,
|
||||
source_resource_id: row.source_resource_id,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(row.asset_kind),
|
||||
asset_kind: row.asset_kind,
|
||||
generation_inputs_json: row.generation_inputs_json,
|
||||
thumbnail_src: row.thumbnail_src,
|
||||
generation_cost_mud_points: row.generation_cost_mud_points,
|
||||
@@ -7847,7 +7846,7 @@ fn asset_snapshot_from_row(ctx: &ReducerContext, row: EditorAsset) -> EditorAsse
|
||||
model: row.model,
|
||||
provider: row.provider,
|
||||
task_id: row.task_id,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(row.asset_kind),
|
||||
asset_kind: row.asset_kind,
|
||||
generation_inputs_json: row.generation_inputs_json,
|
||||
source_resource_id: row.source_resource_id.or(inferred_source_resource_id),
|
||||
public_showcase_enabled,
|
||||
@@ -7889,7 +7888,7 @@ fn admin_asset_snapshot_from_row(
|
||||
model: row.model,
|
||||
provider: row.provider,
|
||||
task_id: row.task_id,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(row.asset_kind),
|
||||
asset_kind: row.asset_kind,
|
||||
generation_inputs_json: row.generation_inputs_json,
|
||||
source_resource_id: row.source_resource_id,
|
||||
thumbnail_src: row.thumbnail_src,
|
||||
@@ -8353,10 +8352,6 @@ fn normalize_optional(value: Option<String>) -> Option<String> {
|
||||
.filter(|item| !item.is_empty())
|
||||
}
|
||||
|
||||
fn normalize_editor_semantic_asset_kind(value: Option<String>) -> Option<String> {
|
||||
normalize_optional(value).filter(|asset_kind| asset_kind != "image")
|
||||
}
|
||||
|
||||
fn normalize_title(value: &str) -> String {
|
||||
let title = value.trim();
|
||||
if title.is_empty() {
|
||||
@@ -11859,8 +11854,7 @@ fn normalize_editor_character_animation_row(
|
||||
image_sequence_duration_ms: Option<u64>,
|
||||
evidence: EditorCharacterAnimationObjectEvidence,
|
||||
) -> EditorCharacterAnimationRowPlan {
|
||||
let stored_asset_kind = normalize_editor_semantic_asset_kind(asset_kind.map(str::to_string));
|
||||
let normalized_ordinary_image_kind = asset_kind == Some("image");
|
||||
let stored_asset_kind = asset_kind.map(str::to_string);
|
||||
let formal_frames = parse_migration_frames_json(image_sequence_frames_json);
|
||||
let generation_value =
|
||||
generation_inputs_json.and_then(|value| serde_json::from_str::<JsonValue>(value).ok());
|
||||
@@ -11943,7 +11937,7 @@ fn normalize_editor_character_animation_row(
|
||||
generation_inputs_json: generation_inputs_json.map(str::to_string),
|
||||
image_sequence_frames_json: image_sequence_frames_json.map(str::to_string),
|
||||
image_sequence_duration_ms,
|
||||
changed: normalized_ordinary_image_kind,
|
||||
changed: false,
|
||||
backfilled_frames: false,
|
||||
backfilled_duration: false,
|
||||
reclassified_preview: false,
|
||||
@@ -13123,7 +13117,7 @@ fn canonical_action_asset_for_legacy_canvas_layer(
|
||||
source_resource_id: asset.source_resource_id,
|
||||
created_at: ctx.timestamp,
|
||||
updated_at: ctx.timestamp,
|
||||
asset_kind: normalize_editor_semantic_asset_kind(asset.asset_kind),
|
||||
asset_kind: asset.asset_kind,
|
||||
generation_inputs_json: asset.generation_inputs_json,
|
||||
public_showcase_enabled: false,
|
||||
image_sequence_frames_json: asset.image_sequence_frames_json,
|
||||
@@ -14395,14 +14389,14 @@ mod tests {
|
||||
let inputs_json = inputs.to_string();
|
||||
|
||||
let plan = normalize_editor_character_animation_row(
|
||||
Some("image"),
|
||||
None,
|
||||
Some(inputs_json.as_str()),
|
||||
None,
|
||||
None,
|
||||
EditorCharacterAnimationObjectEvidence::default(),
|
||||
);
|
||||
|
||||
assert!(plan.changed);
|
||||
assert!(!plan.changed);
|
||||
assert!(plan.blocker.is_none());
|
||||
assert_eq!(plan.asset_kind, None);
|
||||
assert_eq!(
|
||||
|
||||
@@ -530,16 +530,6 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canOpenRedrawPanel({
|
||||
...generatedLayer,
|
||||
assetKind: 'image' as CanvasLayer['assetKind'],
|
||||
generationInputs: {
|
||||
fields: [{ title: '画面内容', value: '非场景展示字段' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canOpenRedrawPanel({
|
||||
...generatedLayer,
|
||||
|
||||
@@ -14,8 +14,6 @@ export function resolveUploadedAssetKind(
|
||||
case 'image-sequence':
|
||||
throw new Error('序列帧素材不支持按单个媒体文件上传');
|
||||
case 'image':
|
||||
// 普通静态图片没有 CanvasAssetKind;返回 undefined 让创建请求省略该字段,
|
||||
// 服务端和 SpacetimeDB 按 None 持久化。
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,9 +631,6 @@ 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',
|
||||
);
|
||||
@@ -789,9 +786,6 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
sourceType: 'uploaded',
|
||||
}),
|
||||
);
|
||||
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 !== undefined ? { assetKind } : {}),
|
||||
assetKind,
|
||||
generationInputs,
|
||||
});
|
||||
return createUploadedGenerationReference({
|
||||
@@ -383,7 +383,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
width,
|
||||
height,
|
||||
sourceType: 'uploaded',
|
||||
...(assetKind !== undefined ? { assetKind } : {}),
|
||||
assetKind,
|
||||
generationInputs,
|
||||
});
|
||||
return createUploadedGenerationReference({
|
||||
@@ -1411,9 +1411,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
width: uploadedAsset.width,
|
||||
height: uploadedAsset.height,
|
||||
sourceType: 'uploaded',
|
||||
...(nextLayer.assetKind != null
|
||||
? { assetKind: nextLayer.assetKind }
|
||||
: {}),
|
||||
assetKind: nextLayer.assetKind,
|
||||
generationInputs: nextLayer.generationInputs,
|
||||
})
|
||||
.then((asset) => {
|
||||
|
||||
Reference in New Issue
Block a user