feat(canvas): 云端 Agent 画布布局 - 新增 canvas_arrange_nodes 与新增节点自动落位
云端 Agent 此前只能新建节点与连线,既不会排版也不能移动已有节点:模型只能用几十个
update_node 手算坐标,或者让用户自己拖。
- 新增 backend/internal/canvas/layout:把前端既有的画布布局算法(web/src/lib/canvas/
canvas-layout.ts,本 PR 不改该文件)按同一套常量移植到服务端,提供 Kahn 拓扑分层 flow、
媒体泳道 byType、row/column/grid、对齐与等距、分组分带、AABB 重叠检测与空位搜索。
- 新增写工具 canvas_arrange_nodes:snapshotHash 必需,mode∈{auto,flow,byType,row,column,
grid},可传 nodeIds、groups(只做横向位置分带,不建容器、不改层级)、align、gap、dryRun。
只改坐标,不改内容、不建连线、不增删节点;一次最多 50 个节点 / 12 组。锁定节点、背板等
容器、隐藏批次子节点与已归属背板的子节点保持不动并在回执里说明;整批复用现有 mutation
记录与事件管线,审批与撤销走同一条路径。
- add_node 省略坐标时不再落到 (0,0):优先贴合本批连线的对端(输入在左、输出在右),否则
追加到同泳道同类节点之后,冲突则扫描空位;只给一个轴时保留该轴。agentCanvasOp.X/Y 改为
指针以区分"没给坐标"与"坐标 0"。
- update_node 的 patch 新增 x/y(PatchField.Limit 校验 ±1e6),供模型微调单个节点。
- 系统行为策略新增「布局与整理」一节并升版到 8;docs 补工具说明与工具全集;前端同步审批
预览联合类型、面板文案与徽标。
- 工具 schema 预算按实测从 24000 上调到 25000(21 个工具 24788 字节),并改正注释里
"上游约 23.4 KB"的说法(上游 20 个工具实测 22777 字节)。
This commit is contained in:
1 parent
6f45240599
commit
56598108d4
16 files changed
+1894
-33
No files matched your search
@@ -3,6 +3,8 @@ package capability
|
||||
const (
|
||||
maxAgentNodeTitleRunes = 240
|
||||
maxAgentNodeContentRunes = 16000
|
||||
// 坐标绝对值上限:与 app 侧整理工具收敛几何值的范围一致。
|
||||
maxAgentNodeCoordLimit = 1e6
|
||||
)
|
||||
|
||||
func BuiltinRegistry() *Registry {
|
||||
@@ -60,9 +62,15 @@ func BuiltinRegistry() *Registry {
|
||||
InputKind: "text",
|
||||
Connection: ConnectionPolicy{CanSource: true, CanTarget: true, AcceptedInputKinds: []string{"image"}},
|
||||
CanUpdate: true, SummaryFields: []string{"batchTable"}, DetailFields: []string{"batchTable"}, ProjectionKind: "batch_table", ProjectionField: "batchTable",
|
||||
PatchFields: map[string]PatchField{
|
||||
"title": {Path: "title", Kind: patchKindString, Label: "节点名称", Order: 10, Description: "批量创作表标题", MaxRunes: maxAgentNodeTitleRunes},
|
||||
},
|
||||
PatchFields: func() map[string]PatchField {
|
||||
fields := map[string]PatchField{
|
||||
"title": {Path: "title", Kind: patchKindString, Label: "节点名称", Order: 10, Description: "批量创作表标题", MaxRunes: maxAgentNodeTitleRunes},
|
||||
}
|
||||
for key, field := range positionPatchFields() {
|
||||
fields[key] = field
|
||||
}
|
||||
return fields
|
||||
}(),
|
||||
CreateMetadata: func(string) map[string]any {
|
||||
return map[string]any{
|
||||
"status": "idle",
|
||||
@@ -151,7 +159,7 @@ func generatedMediaSemantics(nodeType string) generatedMediaCapabilitySemantics
|
||||
}
|
||||
|
||||
func editableNodeFields(contentPath, contentLabel, contentDescription string) map[string]PatchField {
|
||||
return map[string]PatchField{
|
||||
fields := map[string]PatchField{
|
||||
"title": {
|
||||
Path: "title", Kind: patchKindString, Label: "节点名称", Order: 10, Description: "节点标题", MaxRunes: maxAgentNodeTitleRunes,
|
||||
},
|
||||
@@ -159,6 +167,19 @@ func editableNodeFields(contentPath, contentLabel, contentDescription string) ma
|
||||
Path: contentPath, Kind: patchKindString, Label: contentLabel, Order: 20, Description: contentDescription, MaxRunes: maxAgentNodeContentRunes,
|
||||
},
|
||||
}
|
||||
for key, field := range positionPatchFields() {
|
||||
fields[key] = field
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// positionPatchFields 是坐标字段:模型可以直接指定节点位置(微调),批量整理走 canvas_arrange_nodes。
|
||||
// 坐标上限与整理侧的收敛范围一致,避免写进离谱的几何值。
|
||||
func positionPatchFields() map[string]PatchField {
|
||||
return map[string]PatchField{
|
||||
"x": {Path: "position.x", Kind: patchKindNumber, Label: "横坐标", Order: 30, Limit: maxAgentNodeCoordLimit, Description: "画布横坐标(像素);与 y 一起移动节点,通常用 canvas_arrange_nodes 批量整理"},
|
||||
"y": {Path: "position.y", Kind: patchKindNumber, Label: "纵坐标", Order: 31, Limit: maxAgentNodeCoordLimit, Description: "画布纵坐标(像素);与 x 一起移动节点,通常用 canvas_arrange_nodes 批量整理"},
|
||||
}
|
||||
}
|
||||
|
||||
func generatedMetadata(prompt string) map[string]any {
|
||||
|
||||
@@ -2,6 +2,7 @@ package capability
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -23,10 +24,12 @@ type ConnectionPolicy struct {
|
||||
}
|
||||
|
||||
type PatchField struct {
|
||||
Path string
|
||||
Kind string
|
||||
Label string
|
||||
Order int
|
||||
Path string
|
||||
Kind string
|
||||
Label string
|
||||
Order int
|
||||
// Limit > 0 时表示数值字段的绝对值上限(坐标用),避免模型写入离谱的几何值。
|
||||
Limit float64
|
||||
Description string
|
||||
MaxRunes int
|
||||
}
|
||||
@@ -107,9 +110,16 @@ func (d Descriptor) ValidatePatch(patch map[string]any) error {
|
||||
return fmt.Errorf("%s 字段 %s 超出长度限制", d.Label, key)
|
||||
}
|
||||
case "number":
|
||||
if _, ok := value.(float64); !ok {
|
||||
number, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s 字段 %s 必须是数字", d.Label, key)
|
||||
}
|
||||
if math.IsNaN(number) || math.IsInf(number, 0) {
|
||||
return fmt.Errorf("%s 字段 %s 不是有效数字", d.Label, key)
|
||||
}
|
||||
if field.Limit > 0 && math.Abs(number) > field.Limit {
|
||||
return fmt.Errorf("%s 字段 %s 超出允许范围(±%g)", d.Label, key, field.Limit)
|
||||
}
|
||||
case "boolean":
|
||||
if _, ok := value.(bool); !ok {
|
||||
return fmt.Errorf("%s 字段 %s 必须是布尔值", d.Label, key)
|
||||
|
||||
Reference in new issue
Block a user