From 1637bb95360eac850a29b9da4e41de126974479e Mon Sep 17 00:00:00 2001 From: AetherNo2332 <130164460+AetherNo2332@users.noreply.github.com> Date: Tue, 22 Sep 2026 01:40:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(cloud-agent):=20=E5=B8=83=E5=B1=80=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8F=82=E6=95=B0=E9=94=99=E8=AF=AF=E6=8C=89=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=BD=92=E7=B1=BB=EF=BC=8C=E5=AE=A1=E6=89=B9=E5=8D=A1?= =?UTF-8?q?=E8=A1=A5=E6=95=B4=E7=90=86=E5=BE=BD=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - canvas_arrange_nodes 的参数契约错误改用上游既有的 cloudAgentFieldError: snapshotHash 缺失按 required、mode/align/gap/nodeIds/groups 非法按 invalid_value、 nodeIds/groups 超限按 item_count。原先一律 BadAuthRequest(*AppError),按上游运行期的 errors.As 顺序会走 failCloudAgentAdmission 把整轮判死,模型拿不到可纠正回执。 - 审批卡补 .canvas-agent-approval-operation-arrange_nodes 徽标配色(与新增/修改/连线同一组), 并补一条前端用例:arrange_nodes 预览项不得被 operation() 当成未知值丢掉。 --- backend/internal/app/cloud_agent_layout.go | 18 +++++++++--------- .../internal/app/cloud_agent_layout_test.go | 8 +++++--- .../components/canvas/canvas-cloud-agent.css | 1 + web/test/agent-approval-presentation.test.ts | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/backend/internal/app/cloud_agent_layout.go b/backend/internal/app/cloud_agent_layout.go index e55945d7c..df6acabb9 100644 --- a/backend/internal/app/cloud_agent_layout.go +++ b/backend/internal/app/cloud_agent_layout.go @@ -203,7 +203,7 @@ func cloudAgentArrangeMode(value string) (layout.Mode, error) { case "grid": return layout.ModeGrid, nil default: - return "", BadAuthRequest("整理模式无效:只能是 auto、flow、byType、row、column 或 grid") + return "", cloudAgentFieldError("mode", "invalid_value", "整理模式无效:只能是 auto、flow、byType、row、column 或 grid") } } @@ -214,7 +214,7 @@ func cloudAgentArrangeAlign(value string) (layout.AlignMode, error) { case "left", "centerx", "right", "top", "centery", "bottom", "distributex", "distributey": return layout.AlignMode(strings.ToLower(strings.TrimSpace(value))), nil default: - return "", BadAuthRequest("对齐模式无效:只能是 left、centerX、right、top、centerY、bottom、distributeX 或 distributeY") + return "", cloudAgentFieldError("align", "invalid_value", "对齐模式无效:只能是 left、centerX、right、top、centerY、bottom、distributeX 或 distributeY") } } @@ -261,7 +261,7 @@ func cloudAgentArrangeScope(doc map[string]any, args cloudAgentArrangeArgs) ([]l for _, id := range requested { node, ok := byID[id] if !ok { - return nil, nil, BadAuthRequest(fmt.Sprintf("要整理的节点 %s 不存在,请重新读取画布", id)) + return nil, nil, cloudAgentFieldError("nodeIds", "invalid_value", fmt.Sprintf("要整理的节点 %s 不在当前画布,请重新读取画布", id)) } if node.NoMove || !cloudAgentLayoutMovable(index[id]) { skipped = append(skipped, id) @@ -320,7 +320,7 @@ func cloudAgentArrangePositions(doc map[string]any, args cloudAgentArrangeArgs, bands = append(bands, layout.Band{Label: label, Nodes: members, Mode: groupMode}) } if len(bands) == 0 { - return nil, nil, "", BadAuthRequest("分组里没有可整理的节点") + return nil, nil, "", cloudAgentFieldError("groups", "invalid_value", "分组里没有可整理的节点") } positions = layout.Bands(bands, args.Gap) } else { @@ -389,13 +389,13 @@ func prepareCloudAgentArrangeNodes(repo *repository.Repository, userID, canvasID return nil, cloudAgentJSONArgumentError(err) } if strings.TrimSpace(args.SnapshotHash) == "" { - return nil, BadAuthRequest("整理节点需要先读取画布并传入 snapshotHash") + return nil, cloudAgentFieldError("snapshotHash", "required", "整理节点需要先读取画布并传入 snapshotHash") } if len(args.Groups) > cloudAgentArrangeMaxGroups { - return nil, BadAuthRequest(fmt.Sprintf("一次最多分成 %d 组", cloudAgentArrangeMaxGroups)) + return nil, cloudAgentFieldError("groups", "item_count", fmt.Sprintf("一次最多分成 %d 组", cloudAgentArrangeMaxGroups)) } if args.Gap < 0 || args.Gap > cloudAgentArrangeMaxGap { - return nil, BadAuthRequest(fmt.Sprintf("组间距必须在 0 到 %d 之间", cloudAgentArrangeMaxGap)) + return nil, cloudAgentFieldError("gap", "invalid_value", fmt.Sprintf("组间距必须在 0 到 %d 之间", cloudAgentArrangeMaxGap)) } canvas, err := repo.CanvasProjectForUser(userID, canvasID) if err != nil { @@ -419,10 +419,10 @@ func prepareCloudAgentArrangeNodes(repo *repository.Repository, userID, canvasID return nil, err } if len(selected) < 2 { - return nil, BadAuthRequest("可整理的节点少于两个;单个节点请直接用 update_node 指定坐标") + return nil, cloudAgentFieldError("nodeIds", "invalid_value", "可整理的节点少于两个;单个节点请直接用 update_node 指定坐标") } if len(selected) > cloudAgentArrangeMaxNodes { - return nil, BadAuthRequest(fmt.Sprintf("一次最多整理 %d 个节点,请分批或用 nodeIds 指定范围", cloudAgentArrangeMaxNodes)) + return nil, cloudAgentFieldError("nodeIds", "item_count", fmt.Sprintf("一次最多整理 %d 个节点,请分批或用 nodeIds 指定范围", cloudAgentArrangeMaxNodes)) } positions, labels, mode, err := cloudAgentArrangePositions(doc, args, selected) if err != nil { diff --git a/backend/internal/app/cloud_agent_layout_test.go b/backend/internal/app/cloud_agent_layout_test.go index 58da11c9c..9383beec9 100644 --- a/backend/internal/app/cloud_agent_layout_test.go +++ b/backend/internal/app/cloud_agent_layout_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "strings" "testing" "infinite-canvas/backend/internal/canvas/layout" @@ -228,11 +227,14 @@ func TestCloudAgentArrangeNodesRejectsStaleSnapshotAndTooManyNodes(t *testing.T) many = append(many, layoutNode(fmt.Sprintf("text-%d", index), "text", float64(index), 0)) } s2, doc2 := layoutFixture(t, many) + // 参数契约类错误同样按字段错误归类:模型因此拿到"哪个字段错、怎么改"的可纠正回执, + // 而不是被走准入失败分支判死整轮。 + var countErr *cloudAgentFieldArgumentError if _, err := applyCloudAgentArrangeNodes(s2.repo, "user", "layout-canvas", arrangeCall(t, map[string]any{ "snapshotHash": creationHash(doc2), "mode": "row", - }), mustRuntimePolicy(t, s2)); err == nil || !strings.Contains(err.Error(), "最多整理") { - t.Fatalf("超限应被拒绝:%v", err) + }), mustRuntimePolicy(t, s2)); !errors.As(err, &countErr) || countErr.Field != "nodeIds" || countErr.Issue != "item_count" { + t.Fatalf("超限应按 nodeIds/item_count 归类:%v", err) } } diff --git a/web/src/components/canvas/canvas-cloud-agent.css b/web/src/components/canvas/canvas-cloud-agent.css index 434344ce6..ed8edf58c 100644 --- a/web/src/components/canvas/canvas-cloud-agent.css +++ b/web/src/components/canvas/canvas-cloud-agent.css @@ -931,6 +931,7 @@ .canvas-agent-approval-operation-add_node { background: #dcfce7; color: #166534; } .canvas-agent-approval-operation-update_node { background: #dbeafe; color: #1d4ed8; } .canvas-agent-approval-operation-connect_nodes { background: #fef3c7; color: #92400e; } +.canvas-agent-approval-operation-arrange_nodes { background: #ccfbf1; color: #115e59; } .canvas-agent-approval-operation-generate_media { background: #f3e8ff; color: #7e22ce; } .canvas-agent-approval-node, diff --git a/web/test/agent-approval-presentation.test.ts b/web/test/agent-approval-presentation.test.ts index f815881b8..1a4c16666 100644 --- a/web/test/agent-approval-presentation.test.ts +++ b/web/test/agent-approval-presentation.test.ts @@ -64,6 +64,20 @@ describe("Agent approval presentation", () => { expect(JSON.stringify(view)).not.toContain("private"); }); + it("keeps an arrange_nodes preview item instead of dropping it as an unknown operation", () => { + // 未知 operation 会被 operation() 判成 null 并丢掉整条预览项,因此新增的 + // arrange_nodes 必须同时进联合类型与判据,否则审批卡只剩"生成"这个兜底标签。 + const view = agentApprovalPresentation({ preview: { + kind: "canvas_mutation", + title: "确认整理画布", + description: "请确认要整理的节点。", + items: [{ operation: "arrange_nodes", nodeId: "text-1", nodeTitle: "开场分镜", nodeTypeLabel: "文本", summary: "整理《开场分镜》" }], + } }); + expect(view.items).toHaveLength(1); + expect(view.items[0].operation).toBe("arrange_nodes"); + expect(view.items[0].nodeTitle).toBe("开场分镜"); + }); + it("states when an unknown approval cannot identify a target", () => { const view = agentApprovalPresentation({ toolName: "unknown_tool", arguments: {} }); expect(view.items).toHaveLength(0);