Canvas Agent 的核心不是模拟鼠标,也不是让模型返回一整份 Scene JSON,而是:读取受限上下文,生成类型化计划,经宿主策略校验后,通过同一个 CommandBus 修改 Document。
学习目标
- 明确框架、宿主 Agent、模型供应商和业务 Policy 的边界;
- 为大画布构建分层且有界的上下文;
- 区分 Planner、AgentPlan、ToolDef、Command 与 Transaction;
- 把模型输出当作不可信输入进行确定性校验;
- 设计结构验证、视觉验证和 revision 冲突重试。
1. 开源包不内置 Agent
开源 npm 包只提供方法和契约,不包含:
模型 SDK、供应商客户端、系统提示词、API Key
业务节点类型、业务权限、业务审核规则
完整所有权:
Host Prompt / Host Agent / Host Provider
→ CanvasAgentContext
→ PlannerPort
→ untrusted provider output
→ parseRemoteAgentPlan + Host Policy
→ AgentPlan / CanvasCommand[]
→ CommandBus Transaction
→ Document
→ Working Set / Renderer
PlannerPort 可以是远程模型、本地模型、规则引擎或人工审核流程。框架只接收数据,不绑定 OpenAI、Anthropic 或任意模型域名。
2. 五个概念不要混淆
| 概念 | 负责什么 |
|---|---|
| Planner | 把任务和上下文翻译成计划 |
| AgentPlan | 本次准备做什么的结构化数据 |
| ToolDef | 允许的动作 schema 与确定性展开逻辑 |
| CanvasCommand | Document 的最小 create/update/delete 变化 |
| Transaction | 一次完整任务的原子提交与 History 单位 |
用户说“把三张卡片水平排列”:
Prompt
→ AgentPlan: arrange_nodes(ids)
→ ToolDef 校验 ids
→ 本地算法计算坐标
→ 3 条 update Command
→ 1 个 Transaction
模型负责意图和选择,本地确定性代码负责重复坐标计算。这样更便宜、更稳定,也便于设置上限。
3. 为什么不能发送完整大文档
如果 Document 有 50,000 个节点,全量发送会同时造成 Token、隐私和注意力问题。更好的分层:
Focused:选中、任务明确提及、正在编辑,字段较完整
Nearby:Camera/Working Set 附近,只有 ID、类型、Bounds、摘要
Recent:最近修改的少量节点
Peripheral:远区只给聚类数量和包围盒
上下文必须声明边界:
interface CanvasAgentContext {
revision: number
totalNodeCount: number
nodes: CompactNode[]
selectedIds: string[]
viewport: Bounds
truncated: boolean
}
truncated=true 告诉 Planner:未出现不代表不存在。需要远区数据时,应先调用 query 工具,而不是猜 ID。
4. 上下文脱敏由宿主决定
业务 payload 不能默认全部发给上游:
const contextNode = compactNodeForRemoteAgent(productNode, {
compactPayload: (node) => ({
publicId: node.payload.publicId,
label: node.payload.label,
}),
})
宿主应明确 allowlist。内部成本、用户信息、私有文件路径、签名 URL、令牌和不可公开字段不能因为“模型需要上下文”就被透传。
5. ToolDef 比任意 JavaScript 更可靠
任意代码难以回答:
- 能访问哪些节点和字段;
- 一次最多创建多少对象;
- 如何撤销、审计和重试;
- 如何处理不存在的 ID;
- 是否绕过 Document 直接调用 Renderer。
ToolDef 提供白名单:
interface ToolDef<Args> {
name: string
parse(input: unknown): Args
authorize(args: Args, context: HostPolicyContext): void
toCommands(args: Args, document: DocumentSnapshot): CanvasCommand[]
}
常见工具:
query_nodes 读取更多有界上下文
create_nodes 创建少量节点
update_node 修改允许字段
delete_nodes 删除已授权节点
arrange_nodes 本地展开布局
set_viewport 改 Agent/用户视角,不改作品
verify 结构或视觉验收
6. 模型输出必须经过多层校验
JSON 语法/Schema
→ 工具名白名单
→ 数量、文本、坐标、尺寸上限
→ 引用 ID 必须存在或由本计划创建
→ allowedKinds / allowedPatchKeys
→ 宿主 validateNode / authorize
→ expectedRevision
→ CommandBus 最终不变量
示例:
const plan = parseRemoteAgentPlan({
actionId,
expectedRevision: snapshot.revision,
nodes: snapshot.nodes,
content: providerOutput,
policy: {
allowedKinds: ['card', 'connector'],
allowedPatchKeys: ['bounds', 'payload', 'zIndex'],
maxCommands: 100,
validateNode(node) {
if (typeof node.payload.label !== 'string') {
throw new Error('label is required')
}
},
},
})
校验失败不能“尽量执行剩下部分”。一次 AgentPlan 应保持原子性,或显式拆成可审计的多个事务。
7. Revision 冲突与重试
Agent 读取 revision 41
用户修改画布 → revision 42
Agent 提交 expectedRevision 41
→ 拒绝 canvas_revision_conflict
→ 重新 getContext/query
→ 重新规划或 rebase
网络重试使用同一 actionId,防止已提交事务重复执行;内容发生变化则必须使用新 ID。
8. API Key 和模型域名的边界
在线 Demo 允许用户自行填写 OpenAI-compatible endpoint、API Key 和模型。安全要求:
- Key 只保存在当前页面内存,刷新即清空;
- 浏览器不把 Key写入 localStorage/IndexedDB/日志;
- 请求通过受限网关,校验 origin、协议、端口和响应大小;
- 网关设置请求体、并发、速率和超时上限;
- 生产宿主更推荐服务端持有凭据。
框架只提供计划解析和 Command 提交方法,不代理任意 URL,也不替宿主决定供应商。
9. “提交成功”不等于“任务完成”
验证至少三层:
结构验证:节点数、ID、Bounds、关系、字段不变量
行为验证:Transaction 成功、History 可 Undo、revision 正确
视觉验证:截图/导出、重叠、遮挡、文字溢出、人工复核
“把卡片排整齐”即使三条 Command 都成功,也可能视觉上重叠。没有视觉 oracle 时,应返回 needs-review,而不是宣称完成。
10. 操作 Demo
- 在在线画布创建几个节点并选择其中两个;
- 查看发给模型的是有界摘要而非 Renderer 对象;
- 输入 endpoint、Key、model,执行“水平排列选中节点”;
- 观察 AgentPlan 经 Policy 变成一个 Transaction;
- Undo 一次,确认整体撤销;
- 模拟非法 kind、越界坐标和不存在 ID,确认零部分写入;
- 在规划期间人工修改画布,确认 revision conflict;
- 刷新页面,确认 API Key 不被恢复。
11. 引入旧项目的最小路径
已有业务 Store
→ 映射成 CanvasNode 摘要
→ 为现有操作封装 ToolDef
→ ToolDef 输出 CanvasCommand
→ CommandBus 回调现有 Store
→ 保留现有 Renderer
不要求先换 Canvas 框架。第一阶段甚至可以只接一个只读 getContext 和一个低风险 update_node,验证审计与 Undo 后再扩展。
12. 验收标准
包内没有模型 SDK、Prompt 或密钥
上下文有数量/字节上限并携带 truncated
敏感 payload 默认不外发
模型输出经过 Schema + Host Policy + CommandBus
旧 revision 与重复 actionId 行为确定
Agent 与人类操作共用 Transaction/History
结构成功与视觉成功明确区分
常见错误
- 把完整大 Document 发送给模型;
- 让模型直接输出/执行 JavaScript;
- 把 Konva Node 或 ImageBitmap 当上下文;
- 工具粒度过低,让模型生成上千个坐标;
- 信任模型返回的 ID、URL、颜色和数量;
- 把 API Key 持久化到浏览器;
- 只看 HTTP 200 就宣称视觉任务完成。
思考题
当 Agent 需要修改视口外的一组节点时,应该直接扩大全量上下文,还是先 query 并返回聚类/候选?设计一套不超过固定 Token 预算的两阶段工具链。