fix(agent): 画布助手 - 避免未支持节点阻断整张画布
This commit is contained in:
@@ -462,10 +462,13 @@ func cloudAgentCanvasSummary(canvas *model.CanvasProject) (string, error) {
|
||||
break
|
||||
}
|
||||
descriptor, known := cloudAgentNodeCapabilityForType(node.Type)
|
||||
if !known {
|
||||
return "", BadAuthRequest("画布包含当前 Agent 不支持的节点类型")
|
||||
}
|
||||
item := map[string]any{"id": truncateRunes(node.ID, 100), "type": truncateRunes(node.Type, 40), "title": truncateRunes(node.Title, 300)}
|
||||
if !known {
|
||||
item["agentSupported"] = false
|
||||
item["agentUnsupportedReason"] = "仅展示基础信息;当前 Agent 不支持操作此类型节点"
|
||||
nodes = append(nodes, item)
|
||||
continue
|
||||
}
|
||||
projected, err := cloudAgentProjectNodeFields(map[string]any{"title": node.Title}, node.Metadata, descriptor, descriptor.SummaryFields, 600, false, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -86,7 +86,12 @@ func cloudAgentCanvasState(repo *repository.Repository, userID string, doc map[s
|
||||
}
|
||||
capability, known := cloudAgentNodeCapabilityForType(stringValue(node["type"]))
|
||||
if !known {
|
||||
return nil, BadAuthRequest("画布包含当前 Agent 不支持的节点类型")
|
||||
// Read visibility is not permission to mutate or use a node as a media reference.
|
||||
item["agentSupported"] = false
|
||||
item["agentUnsupportedReason"] = "仅展示基础信息;当前 Agent 不支持操作此类型节点"
|
||||
nodes = append(nodes, item)
|
||||
included[id] = true
|
||||
continue
|
||||
}
|
||||
fields := capability.SummaryFields
|
||||
if len(ids) > 0 {
|
||||
|
||||
@@ -1,13 +1,58 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/canvas/capability"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestCloudAgentMixedCanvasReadsUnsupportedNodesWithoutGrantingCapabilities(t *testing.T) {
|
||||
nodes := []map[string]any{{"id": "text", "type": "text", "metadata": map[string]any{"content": "readable"}}}
|
||||
for _, kind := range []string{"ai-art-critique", "config", "drawing", "future-plugin"} {
|
||||
nodes = append(nodes, map[string]any{"id": kind, "type": kind, "title": "插件节点", "position": map[string]any{"x": 10.0, "y": 20.0}, "metadata": map[string]any{"content": "PRIVATE_SENTINEL", "apiKey": "PRIVATE_SENTINEL"}})
|
||||
if _, supported := cloudAgentNodeCapabilityForType(kind); supported {
|
||||
t.Fatalf("unsupported type gained write capability: %s", kind)
|
||||
}
|
||||
if err := validateCreationOps([]CreationCanvasOp{{Type: "add_node", ID: "new", NodeType: kind}}); err == nil {
|
||||
t.Fatalf("unsupported creation accepted: %s", kind)
|
||||
}
|
||||
if _, _, err := cloudAgentReferenceDescriptor(nodes[len(nodes)-1]); err == nil {
|
||||
t.Fatalf("unsupported media reference accepted: %s", kind)
|
||||
}
|
||||
if err := validateCloudAgentConnection(nodes, kind, "text"); err == nil {
|
||||
t.Fatalf("unsupported connection accepted: %s", kind)
|
||||
}
|
||||
}
|
||||
doc := map[string]any{"nodes": nodes, "connections": []map[string]any{{"id": "edge", "fromNodeId": "drawing", "toNodeId": "text"}}}
|
||||
raw, _ := json.Marshal(doc)
|
||||
summary, err := cloudAgentCanvasSummary(&model.CanvasProject{PayloadJSON: string(raw)})
|
||||
if err != nil || strings.Contains(summary, "PRIVATE_SENTINEL") || !strings.Contains(summary, `"agentSupported":false`) {
|
||||
t.Fatalf("mixed summary failed or leaked metadata: %v", err)
|
||||
}
|
||||
for _, ids := range [][]string{nil, {"drawing", "config"}} {
|
||||
view, err := cloudAgentCanvasState(nil, "user", doc, 0, ids, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := view.(map[string]any)
|
||||
encoded, _ := json.Marshal(result)
|
||||
if strings.Contains(string(encoded), "PRIVATE_SENTINEL") || result["snapshotHash"] != cloudAgentCanvasHash(doc) || len(result["connections"].([]any)) != 1 {
|
||||
t.Fatal("unsafe projection or lost snapshot/connection")
|
||||
}
|
||||
want := len(nodes)
|
||||
if ids != nil {
|
||||
want = len(ids)
|
||||
}
|
||||
if len(result["nodes"].([]any)) != want {
|
||||
t.Fatal("nodes silently omitted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudAgentToolsFollowCanvasCapabilityRegistry(t *testing.T) {
|
||||
req := agentTestRequest()
|
||||
req.PermissionMode = "auto"
|
||||
|
||||
@@ -34,6 +34,7 @@ description: 信封、错误码与 OpenAPI
|
||||
- `GET /agent/runs/:id` 返回运行信息;`POST /agent/runs/:id/cancel` 取消当前运行及活动子任务;`POST /agent/runs/:id/approvals/:approvalId/decision` 接受 `{"decision":"approve"|"reject","reason":"..."}`,审批决定幂等且服务端校验归属。
|
||||
- `GET /agent/runs/:id/events` 返回 `agent_event` 和首次 / 状态版本变化时的紧凑 `run_snapshot` SSE;无变化时只发送 heartbeat。事件 `Seq` 在运行内递增;支持 `?after=` / `Last-Event-ID`,游标不是运行 ID。`cleanupPending: true` 时即使运行已进入终态也会继续推送,直到后台收尾完成。前端应以快照恢复状态,代理只对这一精确路径关闭缓冲。
|
||||
- `generation_task_created` 携带 `canvasId`、`nodeId`、`taskId`、`title`、`mode`、`referenceNodeIds`。草稿、提交、画布工具写入,以及媒体成功 / 失败 / 取消的真实节点变更,均在画布写入与运行检查点的同一事务持久化 `canvas_updated`。节点已删除或换任务时不伪造更新;`run_failed` 本身不代表节点媒体任务失败。
|
||||
- 未注册 Agent 能力的节点不阻断整张画布读取:摘要和 `canvas_get_state` 仅返回基础信息,并标记 `agentSupported: false` 与原因;不暴露任意 metadata,不授予该类型的创建、修改、连线或媒体引用能力。
|
||||
- `approval.preview` 和 `canvas_updated.actions` 是服务端根据同一解析、快照与 Capability 规划流程生成的解释信息,不是授权凭证。审批卡片明确列出新增/修改节点、节点类型、修改字段、名称变化和连线两端;执行事件保存同样的可读信息,供历史消息显示和点击定位。实际写入仍重新校验用户归属、`snapshotHash`、Capability、节点锁、预算和审批 `callHash`。
|
||||
- `canvas_updated` 包含 `canvasId`、`operation`、`callId`、`actions`、可选的 `preview`/`text` 和 `canvasPatch`。`canvasPatch` 为 `{ canvasId, updatedAt, nodes: [{ before, after }], connections: [{ before, after }] }`;新增对象的 `before` 为 `null`,仅包含有变化的节点和连线。当前工具不开放删除。节点 action 保存操作、节点 ID、执行前标题、类型、修改字段和可选的修改后标题;引用 action 同时保存来源与目标节点 ID、标题和类型,不要求前端从内部 ID 猜测对象。`callId` 用于合并同一画布操作的工具完成消息。
|
||||
- 前端对增量按 40ms 窗口合批,三方合并远端基线、当前 Store 和编辑器,正常完成 / 失败无需全量 GET;保留无关节点编辑、拖动与视口,同字段冲突、删除或新任务绑定不强行覆盖。只有事件缺口、断流、增量冲突或没有增量的事件源需要全量校准,同一画布协调器最多每秒发起一次,单请求在途,期间重复通知合并为一次尾随校准。已识别增量协议后,相邻 `tool_failed` / `run_failed` 不重复触发整图刷新。
|
||||
|
||||
@@ -5,6 +5,11 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
|
||||
# 待测试
|
||||
|
||||
## 混合节点画布 Agent
|
||||
|
||||
- 未注册 Agent 能力的节点(包括插件、配置、绘图节点)不再阻断整张画布的 Agent 摘要与分页读取;返回基础信息和 `agentSupported: false`,不读取任意 metadata,也不授予创建、修改或媒体引用能力。
|
||||
- 已补混合类型摘要、列表与指定节点读取、私有字段隔离、快照/连线保留和未支持类型创建拒绝测试。待真实账号验证混合画布能启动 Agent 并创建受支持的图片节点;不代表新增了插件节点编辑或全画布布局工具。
|
||||
|
||||
## 画布 Agent 预算与按需技能
|
||||
|
||||
- 已取消前后端最多 8 个技能、生成预算 8 / 120 秒和固定模型循环次数约束;生成数与视频累计秒数默认 0 表示不限,积分、权限、生成审批和供应商实际能力仍强校验。
|
||||
|
||||
Reference in New Issue
Block a user