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:
AetherNo2332
2026-09-22 01:32:32 +08:00
parent 56598108d4
commit 6a216dddc2
2 changed files with 125 additions and 1 deletions

View File

@@ -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 {

View 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)
}
}