diff --git a/backend/internal/app/cloud_agent_canvas_events.go b/backend/internal/app/cloud_agent_canvas_events.go index 1458c4ab3..7b5b4e267 100644 --- a/backend/internal/app/cloud_agent_canvas_events.go +++ b/backend/internal/app/cloud_agent_canvas_events.go @@ -26,12 +26,65 @@ func cloudAgentObjectChanges(before, after any) []map[string]any { for _, item := range creationMaps(after) { old := previous[stringValue(item["id"])] if !reflect.DeepEqual(old, item) { - changes = append(changes, map[string]any{"before": old, "after": item}) + oldFields, newFields := cloudAgentShrinkChange(old, item) + changes = append(changes, map[string]any{"before": oldFields, "after": newFields}) } } return changes } +// cloudAgentGeometryFields 是"只改位置/尺寸"的字段集合。 +var cloudAgentGeometryFields = []string{"position", "width", "height", "zIndex"} + +// cloudAgentShrinkChange 把"只动了几何"的节点变更缩成几何增量。 +// +// 画布增量在前端是**三方合并**(按字段合并,未出现的字段保持本地值),所以只发发生变化的 +// 几何字段既安全又必要:一次整理 50 个节点时,若每个节点都带完整正文(分镜行、Markdown +// 正文可达十几 KB),单条 canvas_updated 事件就会超过 validateCloudAgentRuntime 的 +// 单事件 128KiB 上限("Agent runtime event payload is too large"),整轮直接终止。 +// 只有"除几何外完全一致"时才缩:带内容/metadata 变化的节点仍发完整 before/after, +// 以免影响前端对生成任务状态的保护逻辑。 +func cloudAgentShrinkChange(before, after map[string]any) (map[string]any, map[string]any) { + if before == nil || after == nil { + return before, after + } + for key, value := range after { + if cloudAgentContainsString(cloudAgentGeometryFields, key) { + continue + } + if !reflect.DeepEqual(before[key], value) { + return before, after + } + } + for key, value := range before { + if cloudAgentContainsString(cloudAgentGeometryFields, key) { + continue + } + if _, exists := after[key]; !exists && value != nil { + return before, after + } + } + shrunkBefore := map[string]any{"id": after["id"]} + shrunkAfter := map[string]any{"id": after["id"]} + for _, key := range cloudAgentGeometryFields { + final, hasFinal := after[key] + initial, hasInitial := before[key] + if !hasFinal && !hasInitial { + continue + } + if reflect.DeepEqual(initial, final) { + continue + } + if hasInitial { + shrunkBefore[key] = initial + } + if hasFinal { + shrunkAfter[key] = final + } + } + return shrunkBefore, shrunkAfter +} + func emitCloudAgentCanvasChange(repo *repository.Repository, runID string, state *cloudAgentRuntime, input cloudAgentMutationInput) error { before, err := creationDocument(input.BeforeJSON) if err != nil { diff --git a/backend/internal/app/cloud_agent_layout_patch_test.go b/backend/internal/app/cloud_agent_layout_patch_test.go new file mode 100644 index 000000000..69f8fbc73 --- /dev/null +++ b/backend/internal/app/cloud_agent_layout_patch_test.go @@ -0,0 +1,71 @@ +package app + +import ( + "encoding/json" + "reflect" + "strings" + "testing" +) + +// geometryPatchFixture 造一对"只差位置"的重节点:正文很大,几何变化很小。 +func geometryPatchFixture() (map[string]any, map[string]any) { + heavy := strings.Repeat("分镜行正文", 2000) + node := func(x, y float64, rows []any) map[string]any { + return map[string]any{ + "id": "sb-1", "type": "script", "title": "第一场", + "position": map[string]any{"x": x, "y": y}, "width": 920.0, "height": 360.0, + "metadata": map[string]any{"storyboard": map[string]any{"rows": rows}, "status": "idle"}, + } + } + return node(0, 0, []any{heavy}), node(120, 480, []any{heavy}) +} + +// 位置/尺寸变化只发几何增量:一次整理几十个分镜节点时,完整 before/after 会让单条 +// canvas_updated 事件超过 validateCloudAgentRuntime 的 128KiB 单事件上限,整轮直接终止。 +func TestCloudAgentGeometryOnlyChangeShrinksPatch(t *testing.T) { + before, after := geometryPatchFixture() + shrunkBefore, shrunkAfter := cloudAgentShrinkChange(before, after) + raw, _ := json.Marshal([]map[string]any{{"before": shrunkBefore, "after": shrunkAfter}}) + if len(raw) > 512 { + t.Fatalf("几何增量仍然过大(%d 字节):%s", len(raw), raw) + } + if shrunkBefore["id"] != "sb-1" || shrunkAfter["id"] != "sb-1" { + t.Fatalf("增量必须带 id:%+v %+v", shrunkBefore, shrunkAfter) + } + if _, ok := shrunkAfter["position"]; !ok { + t.Fatalf("几何增量缺少 position:%+v", shrunkAfter) + } + if _, ok := shrunkAfter["metadata"]; ok { + t.Fatalf("几何增量不应携带正文字段:%+v", shrunkAfter) + } + + // 新增节点(before 为空)没有可比对的旧值,不缩。 + if _, kept := cloudAgentShrinkChange(nil, after); kept["metadata"] == nil { + t.Fatalf("新增节点不应被缩减:%+v", kept) + } +} + +// 内容变化(正文 / metadata)仍发完整 before/after:前端按字段三方合并,缩成几何增量会 +// 丢掉内容变化,也会影响前端对生成任务状态的保护逻辑。 +func TestCloudAgentContentChangeKeepsFullPatch(t *testing.T) { + before, after := geometryPatchFixture() + contentAfter := map[string]any{ + "id": "sb-1", "type": "script", "title": "第一场", + "position": after["position"], "width": 920.0, "height": 360.0, + "metadata": map[string]any{"storyboard": map[string]any{"rows": []any{"改过的行"}}, "status": "idle"}, + } + keptBefore, keptAfter := cloudAgentShrinkChange(before, contentAfter) + if _, ok := keptAfter["metadata"]; !ok { + t.Fatalf("内容变化被错误地缩成几何增量:%+v", keptAfter) + } + if keptBefore["id"] != before["id"] || keptBefore["metadata"] == nil { + t.Fatalf("内容变化应保留完整 before:%+v", keptBefore) + } + + // 键被删掉时同样不缩:只发几何字段会让前端无法区分"消失的字段"与"保持本地值"。 + keyRemoved := map[string]any{"id": "sb-1", "type": "script", "title": "第一场", "position": after["position"]} + deletedBefore, deletedAfter := cloudAgentShrinkChange(before, keyRemoved) + if !reflect.DeepEqual(deletedBefore, before) || !reflect.DeepEqual(deletedAfter, keyRemoved) { + t.Fatalf("缺键的变更不应被缩成几何增量:%+v %+v", deletedBefore, deletedAfter) + } +}