修正停发 model 的适用范围并补齐读写对称的服务端断言
只对已登记项目资源的图层停发 model 与 provider 缺资源行的自包含本地图片序列继续携带,避免角色动画层元数据永久丢失 补测 module 读回确实丢弃 sourceType 键,以及显式冲突的 sourceType 仍失败关闭 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -372,6 +372,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
originalHeight: 768,
|
||||
zIndex: 9,
|
||||
sourceType: 'generated',
|
||||
resourcePersistenceState: 'registered',
|
||||
objectKey: 'generated/object.png',
|
||||
model: 'birefnet',
|
||||
sourceResourceId: 'resource-provider-source',
|
||||
@@ -390,8 +391,8 @@ describe('ImageCanvasEditorModel', () => {
|
||||
expect(snapshot).not.toHaveProperty('assetKind');
|
||||
expect(snapshot.assetKindOverride).toBeNull();
|
||||
expect(snapshot).not.toHaveProperty('generationInputs');
|
||||
// 服务端读边界会脱敏内部处理模型并省略 provider,客户端拿到的 model 是按来源链推导出的
|
||||
// 展示值;回写它会与资源行原值冲突并让整次结构化保存 400。
|
||||
// 有资源行时,服务端读边界会脱敏内部处理模型并省略 provider,客户端拿到的 model 是按来源
|
||||
// 链推导出的展示值;回写它会与资源行原值冲突并让整次结构化保存 400。
|
||||
expect(snapshot).not.toHaveProperty('model');
|
||||
expect(snapshot).not.toHaveProperty('provider');
|
||||
|
||||
@@ -435,6 +436,45 @@ describe('ImageCanvasEditorModel', () => {
|
||||
expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士');
|
||||
});
|
||||
|
||||
it('keeps model metadata on self-contained local sequences that have no resource row', () => {
|
||||
// 中文注释:角色动画逐帧层用 local- 资源 id、image-sequence、无 objectKey,服务端
|
||||
// normalize 走 resource == None 早退分支,只摘 assetKind 就把 item 原样写回——item_json
|
||||
// 是这类图层元数据的唯一存储。停发 model 会让它在下一次保存后永久丢失。
|
||||
const layer: CanvasLayer = {
|
||||
id: 'layer-character-animation',
|
||||
resourceId: 'local-resource-character-animation-1',
|
||||
title: '角色动作',
|
||||
src: '/generated/sequence/frame01.png',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 320,
|
||||
height: 240,
|
||||
originalWidth: 320,
|
||||
originalHeight: 240,
|
||||
zIndex: 1,
|
||||
sourceType: 'generated',
|
||||
mediaType: 'image-sequence',
|
||||
imageSequenceFrames: [
|
||||
{
|
||||
frameIndex: 1,
|
||||
imageSrc: '/generated/sequence/frame01.png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
],
|
||||
model: 'seedance2.0-fast',
|
||||
provider: 'ark',
|
||||
};
|
||||
|
||||
const snapshot = serializeLayer(layer);
|
||||
expect(snapshot.model).toBe('seedance2.0-fast');
|
||||
expect(snapshot.provider).toBe('ark');
|
||||
|
||||
const hydrated = hydrateLayer(snapshot, new Map());
|
||||
expect(hydrated?.model).toBe('seedance2.0-fast');
|
||||
expect(serializeLayer(hydrated!).model).toBe('seedance2.0-fast');
|
||||
});
|
||||
|
||||
it('keeps the resource sourceType across a structured layout round trip', () => {
|
||||
const layer: CanvasLayer = {
|
||||
id: 'layer-generated',
|
||||
|
||||
@@ -281,6 +281,26 @@ function serializeImageSequenceFrames(
|
||||
return serializedFrames.length ? serializedFrames : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文注释:`model` / `provider` 只有在图层挂着项目资源行时才交给资源行权威持有。
|
||||
*
|
||||
* 有资源行:服务端读边界会脱敏内部处理模型(`model`)并无条件省略 `provider`,客户端拿到的
|
||||
* `layer.model` 是脱敏后按来源链推导出的展示值,回写必然与资源行原值冲突,被结构化保存判为
|
||||
* 「与项目资源不一致」而整次 400。保存时服务端本就会剥离这两个字段,也不参与画布布局哈希,
|
||||
* 因此直接不发。
|
||||
*
|
||||
* 没有资源行(自包含的 legacy 本地图片序列,例如角色动画逐帧层):服务端
|
||||
* `normalize_structured_canvas_layer_against_resource` 走的是 `resource == None` 早退分支,
|
||||
* 只摘掉 `assetKind` 就把 item 原样写回,item_json 是这些元数据的**唯一**存储。此时停发会让
|
||||
* 模型信息在下一次保存后永久丢失,图片信息、ZIP 导出与快速编辑默认模型一起静默退化。
|
||||
*/
|
||||
function serializeResourceOwnedModelFields(layer: CanvasLayer) {
|
||||
if (layer.resourcePersistenceState === 'registered') {
|
||||
return {};
|
||||
}
|
||||
return { model: layer.model, provider: layer.provider };
|
||||
}
|
||||
|
||||
export function serializeLayer(layer: CanvasLayer): EditorProjectLayerSnapshot {
|
||||
return {
|
||||
layerId: layer.id,
|
||||
@@ -308,11 +328,7 @@ export function serializeLayer(layer: CanvasLayer): EditorProjectLayerSnapshot {
|
||||
),
|
||||
prompt: layer.prompt,
|
||||
actualPrompt: layer.actualPrompt,
|
||||
// 中文注释:`model` / `provider` 不回写。服务端读边界会脱敏内部处理模型(`model`)并
|
||||
// 无条件省略 `provider`,客户端因此拿不到权威值——`layer.model` 是脱敏后按来源链推导出的
|
||||
// 用户可见模型,回写它必然与资源行原值冲突,被结构化保存判为「与项目资源不一致」而整次
|
||||
// 400。这两个字段本就由资源行权威持有,保存时会被服务端剥离,也不参与画布布局哈希,
|
||||
// 与已经省略的 `src` / `imageSrc` / `assetKind` / `generationInputs` 同类。
|
||||
...serializeResourceOwnedModelFields(layer),
|
||||
taskId: layer.taskId,
|
||||
objectKey: layer.objectKey,
|
||||
assetObjectId: layer.assetObjectId,
|
||||
|
||||
Reference in New Issue
Block a user