读边界只放宽对象字段的 null,Task 判据改用生成模型
两处评审都成立,且都源自同一个毛病:修复时选错了作用范围/抽象层级。 一、数组元素的 null 不该跟着放宽 上一版把「无差别删 null」整体改成「只删本次抹掉的内联媒体」,连数组元素一起放宽了。 布局数组是唯一没有元素级 shape 校验的写入口(legacy layout_storage_version=0 是新画布的 默认状态,写侧 serialize_editor_layers 只挡内联媒体、不校验元素形状),读回的 null 元素会让 前端 canvasLayoutItemId / isCanvasSettingsLayoutItem 抛 TypeError,整次项目套用被 Promise catch 吞成静默失败;generationInputs 的 fields / references 里混进 null 元素也会让整份元数据 被 isRecord 判非法丢弃——与本来要修的 SFX 症状同类。现在数组元素照旧过滤 null,放宽严格 限定在对象字段(requestedDurationSeconds 这种有意义的空值)。 二、Task 判据从资源分类换成生成模型 assetKind 是 assetKindOverride ?? resourceAssetKind,用户改一次「素材标签」就翻转;换成 resolveLayerResourceAssetKind 仍不够:legacy 布局快照没有 assetKindOverride 键时,旧的 assetKind 会被水合成 override 而 resourceAssetKind 落成 null,资源行查不到时同样是 null—— 恰恰是元数据缺失最常伴随的形态。判据要回答的是「这个 taskId 由谁生成」,所以落在 model 上: 资源行权威字段,registered 图层的 serializeLayer 不回写,改标签不影响,后端 canonicalize_editor_sound_effect_model 只接受 eleven_text_to_sound_v2 一个取值。 - 新增后端回归:布局数组与 fields 数组里的 null 元素被过滤,对象字段的 null 原样保留 - 新增前端回归:资源类型不可解析(resourceAssetKind 为 null)的音效层仍显示完整 taskId - 既有的「改标签」两向回归保持不变,在新判据下同样成立 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8856,11 +8856,12 @@ fn sanitize_editor_payload_inline_media(mut value: Value) -> Value {
|
||||
|
||||
/// 返回 `true` 表示这个值本身就是被抹掉的内联媒体,调用方必须把承载它的键或数组元素一并删除。
|
||||
///
|
||||
/// 中文注释:删除范围必须严格限定在「本次抹掉的值」。早期实现在每层对象上无差别执行
|
||||
/// 中文注释:放宽只针对**对象字段**。早期实现在每层对象上无差别执行
|
||||
/// `retain(|_, child| !child.is_null())`,把调用方原本就写着 `null` 的合法契约字段一起删了:
|
||||
/// SFX 自动时长的 `generationInputs.soundEffect.requestedDurationSeconds` 正是这样在读边界
|
||||
/// 消失,客户端据此判定整份 soundEffect 元数据非法,音频信息页整块退化成空。脱敏强度不变
|
||||
/// ——内联媒体仍然先被置空再连键删除,只是不再殃及本来就是 `null` 的字段。
|
||||
/// 消失,客户端据此判定整份 soundEffect 元数据非法,音频信息页整块退化成空。对象字段的 `null`
|
||||
/// 是「有意义的空值」,必须原样下发;**数组元素的 `null` 是无效元素,照旧过滤**(见 Array 分支)。
|
||||
/// 脱敏强度不变——内联媒体仍然先被置空再连键/连元素删除。
|
||||
fn sanitize_editor_payload_media_value(
|
||||
value: &mut Value,
|
||||
inherited_media: Option<&EditorPayloadMediaReference>,
|
||||
@@ -8936,8 +8937,16 @@ fn sanitize_editor_payload_media_value(
|
||||
false
|
||||
}
|
||||
Value::Array(items) => {
|
||||
// 中文注释:数组元素继续沿用「连 null 一起丢掉」的旧行为,放宽只发生在对象字段上。
|
||||
// 布局数组是唯一没有元素级 shape 校验的写入口(legacy layout_storage_version=0 是
|
||||
// 新画布的默认状态),一个 null 元素读回前端后会让 canvasLayoutItemId /
|
||||
// isCanvasSettingsLayoutItem 抛 TypeError,整次项目套用静默失败;generationInputs
|
||||
// 的 fields / references 里混进 null 元素也会让整份元数据被判非法丢弃。两者都是
|
||||
// 「无效元素」而不是「有意义的空值」,读边界照旧过滤掉。
|
||||
items.retain_mut(|item| {
|
||||
!sanitize_editor_payload_media_value(item, inherited_media, resource_media)
|
||||
let sanitized =
|
||||
sanitize_editor_payload_media_value(item, inherited_media, resource_media);
|
||||
!sanitized && !item.is_null()
|
||||
});
|
||||
false
|
||||
}
|
||||
@@ -13991,6 +14000,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_payload_sanitizer_drops_null_array_items_but_keeps_null_object_fields() {
|
||||
// 数组元素的 null 是无效元素:布局数组里留下一个就会让前端 canvasLayoutItemId /
|
||||
// isCanvasSettingsLayoutItem 抛 TypeError,generationInputs 的 fields 里留下一个会让
|
||||
// 整份元数据被判非法。对象字段的 null 是有意义的空值,必须原样下发。
|
||||
let sanitized = sanitize_editor_payload_media(
|
||||
json!([
|
||||
null,
|
||||
{
|
||||
"layerId": "layer-1",
|
||||
"resourceId": "resource-1",
|
||||
"sourceResourceId": null,
|
||||
"generationInputs": {
|
||||
"fields": [null, { "title": "用户描述", "value": "金币落地" }],
|
||||
"references": [],
|
||||
"soundEffect": {
|
||||
"schemaVersion": 2,
|
||||
"durationMode": "auto",
|
||||
"requestedDurationSeconds": null
|
||||
}
|
||||
}
|
||||
},
|
||||
null
|
||||
]),
|
||||
&[],
|
||||
);
|
||||
|
||||
let items = sanitized.as_array().expect("顶层仍是数组");
|
||||
assert_eq!(items.len(), 1, "布局数组里的 null 元素必须被过滤掉");
|
||||
let layer = &items[0];
|
||||
assert_eq!(layer["layerId"], json!("layer-1"));
|
||||
|
||||
let fields = layer["generationInputs"]["fields"]
|
||||
.as_array()
|
||||
.expect("fields 仍是数组");
|
||||
assert_eq!(fields.len(), 1, "fields 里的 null 元素必须被过滤掉");
|
||||
assert_eq!(fields[0]["title"], json!("用户描述"));
|
||||
|
||||
let sound_effect = layer["generationInputs"]["soundEffect"]
|
||||
.as_object()
|
||||
.expect("soundEffect 保留");
|
||||
assert!(sound_effect.contains_key("requestedDurationSeconds"));
|
||||
assert_eq!(sound_effect["requestedDurationSeconds"], Value::Null);
|
||||
assert!(
|
||||
layer
|
||||
.as_object()
|
||||
.expect("图层是对象")
|
||||
.contains_key("sourceResourceId"),
|
||||
"对象字段的 null 要原样保留"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_payload_media_sanitizer_replaces_inline_layer_src_from_resource() {
|
||||
let resources = vec![EditorProjectResourcePayload {
|
||||
|
||||
Reference in New Issue
Block a user