perf(cloud-agent): 画布事件 - 整理类变更只下发几何增量
一次整理几十个节点时,canvas_updated 仍按整节点下发完整 before/after,单条事件会超过 validateCloudAgentRuntime 的 128KiB 单事件上限,整轮直接失败。 - 新增 cloudAgentShrinkChange:仅当 before/after 除 position/width/height/zIndex 之外 完全一致(且无键被删)时,只下发 id 与变化的几何字段;带内容或 metadata 变化的节点仍发 完整 before/after,不影响前端的三方合并与生成任务状态保护。 - 新增用例覆盖"几何变化只发增量"与"内容变化 / 键被删仍发完整对象"。
This commit is contained in:
@@ -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 {
|
||||
|
||||
71
backend/internal/app/cloud_agent_layout_patch_test.go
Normal file
71
backend/internal/app/cloud_agent_layout_patch_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user