1866 lines
60 KiB
Markdown
1866 lines
60 KiB
Markdown
# 无限画布对话框 Agent 模式升级开发文档
|
||
|
||
> 日期:2026-06-22
|
||
> 当前版本:V2 修订版
|
||
> 参考项目:Cowart(https://github.com/zhongerxin/cowart)
|
||
> 目标:把当前“对话框 Agent 能理解画布上下文并生成结果”,升级为“Agent 能通过稳定协议读写画布、填充画布意图、保留版本链、低打扰执行”的可落地系统。
|
||
|
||
---
|
||
|
||
## 1. 背景与结论
|
||
|
||
### 1.1 当前系统基础
|
||
|
||
当前项目已经具备比较完整的画布与 Agent 基础:
|
||
|
||
- 无限画布节点体系:`types/canvas/index.ts`
|
||
- 画布 Zustand 状态:`store/canvas/index.ts`
|
||
- 画布对话框:`components/canvas/CanvasChatPanel.tsx`
|
||
- 画布上下文传入 Chat API:`contextPayload`
|
||
- 目标图解析:`lib/ai/context-memory/media-input.ts`
|
||
- 目标图锁定:`lib/ai/context-memory/target-resolution.ts`
|
||
- Agent 自动规划:`lib/ai/auto-agent-graph.ts`
|
||
- 画布 Agent 澄清门:`lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts`
|
||
|
||
当前能力已经可以做到:
|
||
|
||
- 选中画布图片后在对话框里发起处理
|
||
- 将画布图片作为上下文传给 Agent
|
||
- 解析“左边那张 / 第2张 / 选中的图 / 上一版结果图”
|
||
- 生成图片、编辑图片、生成视频,并在部分链路中同步回画布
|
||
|
||
### 1.2 当前主要问题
|
||
|
||
1. 画布上下文仍偏“请求参数”,不是稳定的 Agent 画布操作协议。
|
||
2. 对话框 Agent 更像“拿画布图片做输入”,还不是“可读写画布的协作者”。
|
||
3. 结果落点缺少明确契约,经常依赖前端自动同步或消息媒体反推。
|
||
4. 缺少画布上的“输出占位 / 生成槽位”概念,用户不能直接在画布中指定生成位置、比例与用途。
|
||
5. 标注、圈选、批注还没有成为 Agent 编辑任务的一等输入。
|
||
6. 版本迭代关系不够显性,原图、标注、新版本之间缺少稳定链路。
|
||
|
||
### 1.3 Cowart 带来的核心启发
|
||
|
||
Cowart 不是功能复杂的产品,而是边界非常清晰的 Agent + Canvas 协作样板:
|
||
|
||
- 用 `AI image holder` 表达“这里要生成图”。
|
||
- 用画布选中态表达“这次操作目标”。
|
||
- 用标注截图表达“怎么改”。
|
||
- 用 MCP 工具读取 selection、插入 image、保存 page-local asset。
|
||
- 默认非破坏式迭代:新结果放在原图旁边,不覆盖、不移动、不删除原图与标注。
|
||
- 没有选中 holder 也继续生成,插入当前页空白区,减少无谓澄清。
|
||
|
||
### 1.4 本项目升级结论
|
||
|
||
不建议替换现有画布引擎,也不建议照搬 Cowart/tldraw。
|
||
|
||
建议做一层 **Canvas Agent Operation Protocol**:
|
||
|
||
- 前端画布继续使用现有节点系统。
|
||
- Agent 不直接读写 Zustand 内部结构。
|
||
- Chat API 通过统一协议接收画布状态摘要与操作意图。
|
||
- 执行层通过统一 Canvas mutation API 写回节点、版本链、边与元数据。
|
||
|
||
一句话目标:
|
||
|
||
> 让无限画布从“AI 结果展示面板”升级成“Agent 可读、可写、可追踪的工作台”。
|
||
|
||
### 1.5 对话框驱动画布的可行性判断
|
||
|
||
可以做到,而且这是本次升级最重要的产品方向。
|
||
|
||
Cowart 证明的关键不是“AI 可以像人一样点击画布”,而是:
|
||
|
||
> AI 不需要模拟鼠标操作 UI,AI 应该通过受控协议读写画布状态。
|
||
|
||
也就是说,对话框驱动画布的正确链路不是:
|
||
|
||
```text
|
||
用户对话 -> AI 控制鼠标 -> 点击画布 -> 拖拽节点
|
||
```
|
||
|
||
而是:
|
||
|
||
```text
|
||
用户对话 -> Agent 理解意图 -> 读取画布上下文 -> 生成画布操作计划 -> 系统安全执行画布 mutation
|
||
```
|
||
|
||
这条路更稳定,也更适合我们现有架构。因为当前系统已经具备:
|
||
|
||
- 前端能知道当前选中节点:`selectedNodeIds`
|
||
- 前端能把画布图片节点传给后端:`canvasImageNodes`
|
||
- 后端能解析目标图:`resolveChatMediaInput()` / `resolveTargetAssets()`
|
||
- Agent 已经能规划生成、编辑、视频等任务:`runAutoAgentGraph()`
|
||
- 画布 store 已支持 `addNode` / `updateNode` / `addEdge`
|
||
- 服务端已有 `canvasProject.canvasData` 持久化
|
||
|
||
因此缺的不是“让 AI 会操作画布”,而是给 Agent 一套明确、安全、可审计的画布操作协议。
|
||
|
||
第一版要优先支持这些对话驱动画布动作:
|
||
|
||
- “在这个空框里生成一张主图。”
|
||
- “把选中图片背景换成白色,新图放右边。”
|
||
- “按批注修改这张图。”
|
||
- “给这三张图各出一版变体。”
|
||
- “把上一版结果放到当前画布中心。”
|
||
- “把这两张图连成参考关系。”
|
||
- “创建一个视频槽位并填充生成结果。”
|
||
- “帮我聚焦到正在编辑的这张图。”
|
||
- “把画布缩小一点,我想看全局。”
|
||
- “放大选中的主体图,方便继续编辑。”
|
||
|
||
首期只开放非破坏式操作:
|
||
|
||
- 填充空 slot
|
||
- 插入到锚点旁边
|
||
- 追加版本
|
||
- 自动连线
|
||
- 更新文本节点
|
||
- 聚焦 / 缩放 / 平移当前画布视角
|
||
|
||
替换、删除、覆盖、批量移动等高风险动作必须后置,并通过确认门控制。
|
||
|
||
### 1.6 最终产品定位:Agent 模型直接驱动画布
|
||
|
||
本次升级的最终目标不是“生成结果后同步到画布”,而是让 Agent 模型具备和参考项目类似的直接画布操作能力:
|
||
|
||
> 用户只描述需求,Agent 模型理解画布状态、决定操作步骤,并通过受控操作协议直接改变画布内容和视角。
|
||
|
||
这里的“直接操作画布”不是让模型任意修改前端状态,也不是让模型模拟鼠标,而是采用更可靠的分工:
|
||
|
||
- **模型负责决策**:理解用户需求、识别目标节点、选择操作类型、生成操作计划。
|
||
- **系统负责执行**:校验权限、限制风险、计算布局、写入节点、更新视角。
|
||
- **用户负责确认高风险动作**:覆盖、删除、替换、批量移动等必须可拦截。
|
||
|
||
最终体验应该支持:
|
||
|
||
- Agent 根据“这张图”“左边那张”“上一版结果”“选中的图”自动锁定目标。
|
||
- Agent 根据需求自动创建、填充或追加图片/视频/UI slot。
|
||
- Agent 能新增、更新、移动、连接画布节点。
|
||
- Agent 能按批注、框选、箭头、文字说明修改图片。
|
||
- Agent 能生成新版本并自动建立版本关系。
|
||
- Agent 能控制画布视角,把用户带到当前编辑对象。
|
||
- Agent 能在多图场景下整理、分组、对比、聚焦关键结果。
|
||
|
||
目标链路应该从:
|
||
|
||
```text
|
||
用户需求 -> Agent 生成图片/视频 -> 前端把结果同步回画布
|
||
```
|
||
|
||
升级为:
|
||
|
||
```text
|
||
用户需求 -> Agent 生成 CanvasOperationPlan + CanvasViewOperationPlan -> 系统执行画布操作 -> 用户看到画布被直接更新
|
||
```
|
||
|
||
这也是后续所有实现要围绕的主线。
|
||
|
||
---
|
||
|
||
## 2. 产品目标
|
||
|
||
### 2.1 用户体验目标
|
||
|
||
用户在画布对话框中可以自然完成这些任务:
|
||
|
||
- “在这个空框里生成一张主图。”
|
||
- “按选中这张做一版白底图。”
|
||
- “把右边那张的背景改成夏天户外。”
|
||
- “按我画的箭头和批注修改。”
|
||
- “再出三版放到这张图右边。”
|
||
- “保留原图和标注,新图放旁边。”
|
||
- “把这几张都统一成同一风格。”
|
||
- “放大当前编辑对象。”
|
||
- “缩小画布看整体。”
|
||
- “把选中的图移动到视野中央。”
|
||
|
||
### 2.2 系统体验目标
|
||
|
||
- Agent 首先利用画布状态,而不是反复询问用户。
|
||
- 非破坏性任务默认执行。
|
||
- Agent 能控制当前画布视角,帮助用户在大量图片中定位编辑对象。
|
||
- 高风险任务才澄清,例如覆盖、删除、多图不明确、批量编辑不明确。
|
||
- 所有 Agent 写回画布的动作都有可追踪 metadata。
|
||
- 所有生成结果都能回溯到源节点、提示词、模型、任务、版本链。
|
||
|
||
### 2.3 Agent 直接操作画布能力分层
|
||
|
||
为了避免一次性做成高风险大工程,建议按能力等级推进。
|
||
|
||
L1:结果落画布
|
||
|
||
- Agent 生成图片/视频后,系统自动插入画布。
|
||
- 支持插入到选中图右侧、当前视口中心、选中 slot。
|
||
- 默认不覆盖原节点。
|
||
|
||
L2:画布结构操作
|
||
|
||
- Agent 能创建 slot、文本节点、图片节点、视频节点。
|
||
- Agent 能给节点建立 reference / revision 边。
|
||
- Agent 能按版本链组织“原图 -> 标注 -> 新版本”。
|
||
|
||
L3:视角操作
|
||
|
||
- Agent 能放大、缩小、平移、聚焦节点、聚焦选区。
|
||
- 生成或编辑完成后自动把用户带到新结果。
|
||
- 多图对比时自动缩放到一组节点的包围盒。
|
||
|
||
L4:内容编辑操作
|
||
|
||
- Agent 能按批注、框选、箭头、文字说明修改图片。
|
||
- Agent 能更新文本节点内容。
|
||
- Agent 能基于已有节点生成新变体。
|
||
|
||
L5:布局与整理操作
|
||
|
||
- Agent 能按用户要求整理节点布局,例如三列排列、按版本分组、把参考图放左侧、结果放右侧。
|
||
- 这一级涉及移动多个节点,默认需要确认。
|
||
|
||
L6:高风险替换操作
|
||
|
||
- Agent 能替换已有节点媒体、删除节点、覆盖结果。
|
||
- 必须有确认门、撤销能力和操作审计。
|
||
|
||
首期建议实现 L1 + L3 的核心能力,并补上 L2 中的 revision/link,这样最快形成“Agent 真的在操作画布”的体感。
|
||
|
||
---
|
||
|
||
## 3. 范围定义
|
||
|
||
### 3.1 本期要做
|
||
|
||
1. 新增画布 Agent 操作协议类型。
|
||
2. 新增 `agent-slot` 能力,用于表达画布上的生成占位。
|
||
3. 新增前端画布上下文摘要 builder,输出更稳定的 selected / viewport / slots / annotations。
|
||
4. 新增后端 Canvas mutation API,用于安全写回画布。
|
||
5. 改造 Agent 生成与编辑链路,支持:
|
||
- 填充选中 slot
|
||
- 插入锚点右侧
|
||
- 创建非破坏式 revision
|
||
- 写入版本边
|
||
6. 新增画布视角控制能力:
|
||
- 聚焦选中节点
|
||
- 聚焦刚生成的新结果
|
||
- 缩放到选区
|
||
- 缩小查看全局
|
||
- 平移到指定锚点附近
|
||
7. 新增标注输入的最小闭环:
|
||
- 前端支持图片节点附带标注摘要
|
||
- Agent 可根据标注摘要执行编辑
|
||
- 结果放到原图旁边
|
||
|
||
### 3.2 本期不做
|
||
|
||
- 不替换现有 CanvasWorkspace。
|
||
- 不引入 tldraw。
|
||
- 不做完整多人协同。
|
||
- 不做完整 MCP 插件化。
|
||
- 不做复杂自由绘图标注编辑器。
|
||
- 不做跨项目画布资产迁移。
|
||
|
||
### 3.3 实施前必须理解的现有画布功能
|
||
|
||
本次升级不能只理解“画布能放图片”,还必须尊重现有工具的真实语义。当前画布至少包含这些功能面:
|
||
|
||
- 普通无限画布:选择、平移、缩放、撤销、重做、添加图片、添加文本、新建画布。
|
||
- 图片节点:上传、预览、选中、移动、缩放、批量下载、添加到对话框、AI 编辑、局部编辑、裁剪、调整、备注、重命名、删除、下载、前后对比。
|
||
- 视频工作流画布:保存、云文件、标记面板、连线工具、添加图片输入、脚本节点、视频输入、工作流节点库、导入导出。
|
||
- 子工作区入口:风格化模式、风格化 PRO、批量处理、实时画布、图像增强模式、3D 生成。
|
||
- 其他节点:文本节点、UI Screen 节点、视频节点、工作流节点、3D 模型节点、3D 世界节点。
|
||
- 对话框工具栏:Agent 模式、AI 详情、生图、视频、附件、深度思考、模型选择、积分、发送。
|
||
|
||
关键实现入口:
|
||
|
||
- 工具栏配置:`lib/canvas-toolbar-config.ts`
|
||
- 主画布工具栏:`app/canvas/[id]/CanvasEditorLegacyPageClient.tsx`
|
||
- 画布渲染与视角:`components/canvas/CanvasWorkspace.tsx`
|
||
- 图片节点浮层工具栏:`components/canvas/nodes/ImageNode.tsx`
|
||
- 画布状态与基础操作:`store/canvas/index.ts`
|
||
|
||
因此 Agent 第一期不应该试图覆盖全部画布功能。更合理的范围是:
|
||
|
||
- 优先覆盖图片节点操作、选区上下文、非破坏式结果插入、版本链、视角聚焦。
|
||
- 视频工作流、实时画布、3D 世界漫游、复杂 UI Screen 编辑先作为上下文可读,后续再开放写操作。
|
||
- 对删除、覆盖、批量移动、导入替换项目等高风险动作默认不开放自动执行。
|
||
|
||
### 3.4 选中图片浮层工具栏语义盘点
|
||
|
||
当前选中图片浮层工具栏由 `ImageNode.tsx` 根据 `canvasToolbarConfig` 渲染。默认内联显示 7 个图片动作,其余进入“更多操作”。“添加对话”是独立按钮,不是复制图片;当前选中图片工具栏里没有“复制图片节点”能力。3D 节点里存在“复制链接”,那是复制 3D/World 资源 URL,不等于复制画布图片节点。
|
||
|
||
图片工具必须按行为类型分层:
|
||
|
||
| 工具 | 当前行为 | 对原节点影响 | Agent 首期策略 |
|
||
| --- | --- | --- | --- |
|
||
| 添加对话 | 将当前图片解析成对话附件,加入聊天输入上下文 | 不改画布 | 可作为 `attach_to_chat_context`,但通常由用户点击触发 |
|
||
| 识物 | 进入图片点击识别模式,点击局部物体后识别 | 不改画布 | 首期只读上下文,不自动模拟点击 |
|
||
| 高清 | 调用增强服务,在原图右侧生成结果图 | 新建节点 | 可开放为非破坏式 `append_revision` |
|
||
| 抠图 | 移除背景,在原图右侧生成结果图 | 新建节点 | 可开放为非破坏式 `append_revision` |
|
||
| AI 洗图 | 打开快捷面板,按预设生成清洗/重绘结果 | 新建节点 | 可开放,但需把 preset/model/levels 显式协议化 |
|
||
| 文字 | OCR 检测并编辑图片中文字,生成新结果 | 新建节点,同时缓存识别项 | 可开放,但需传递 text edit items |
|
||
| 框选抠图 | 用户框选区域,确认后生成抠图结果 | 新建节点 | 可开放为 `local_region_cutout`,区域坐标必须标准化 |
|
||
| 局部编辑 | 用户框选多个区域,每个区域可有提示词和参考图,提交后生成结果 | 新建节点 | 应作为 Agent 编辑核心能力之一 |
|
||
| 尺寸 | 打开尺寸/标注编辑器,完成后生成新图 | 新建节点 | 可开放为 `resize_image`,需要明确目标尺寸 |
|
||
| 多视角 | 根据角度、俯仰、景别和提示词生成视角变体 | 新建节点 | 可开放为 `generate_angle_variant` |
|
||
| 3D 模型 | 从图片生成 `model3d` 节点并轮询任务 | 新建 3D 节点 | 后置开放,首期只保留结果插入协议兼容 |
|
||
| 3D 世界 | 从图片生成 `world3d` 节点并轮询任务 | 新建 3D 世界节点 | 后置开放,涉及独立 viewer 与资源持久化 |
|
||
| 图层拆分 | 拆出多层图片,叠放在原图右侧 | 新建多图节点 | 可读可展示,自动执行需处理多结果 `mediaItems` |
|
||
| 扩图 | 扩展画幅,在原图右侧生成结果图 | 新建节点 | 可开放为非破坏式 `append_revision` |
|
||
| 裁剪 | 打开裁剪面板,完成后上传裁剪图并替换当前节点 `src` | 修改当前节点 | 高风险,首期不自动执行或必须确认 |
|
||
| 调整 | 打开调色面板,调整值写入当前节点 `metadata.imageAdjustments`,以 CSS filter/overlay 呈现 | 修改当前节点 metadata | 可作为低风险元数据操作,但需要可撤销 |
|
||
| 翻转与旋转 | 生成翻转/旋转后的图片并替换当前节点 `src`,保留 reset 来源 | 修改当前节点 | 高风险,首期不自动执行或必须确认 |
|
||
| 重绘 | 基于当前图片和新提示词生成一张新结果 | 新建节点 | 可开放为 `repaint_from_reference` |
|
||
| 备注 | 打开备注弹窗,保存到节点数据 | 修改当前节点 metadata/data | 可开放为 `update_note` |
|
||
| 改名 | 修改图片节点标题 | 修改当前节点 title | 可开放为低风险 `rename_node` |
|
||
| 下载 | 服务端转成 PS 兼容 PNG 后触发下载 | 不改画布 | 不属于 Agent 画布写操作 |
|
||
| 删除 | 删除图片节点 | 删除节点 | 高风险,必须确认 |
|
||
| 开启对比 | 使用 `originalSrc`、连接源或 compareSourceUrl 打开前后滑杆对比 | 不改画布 | 可作为 view/UI 状态,首期不持久化 |
|
||
| 确认抠图/取消抠图 | 框选抠图模式下确认生成或退出模式 | 确认后新建节点 | 作为 `box_cutout` 子状态处理 |
|
||
|
||
这个盘点直接影响 Agent 操作白名单:
|
||
|
||
- 首期默认允许:高清、抠图、去水印、扩图、重绘、局部编辑、框选抠图、尺寸、多视角、备注、改名、插入新结果、聚焦视角。
|
||
- 首期需要确认:裁剪、翻转旋转、删除、覆盖当前图、批量移动、导入替换。
|
||
- 首期暂不自动化:识物点击、3D 模型、3D 世界、实时画布内部画笔/图层、视频工作流复杂节点执行。
|
||
|
||
### 3.5 对 Agent 操作协议的约束
|
||
|
||
基于现有工具语义,`CanvasOperationPlan` 不能只设计成“add/update/delete node”三类粗操作。至少要表达:
|
||
|
||
- 操作目标:当前选中图、指定节点、slot、区域、最近生成结果、连接源。
|
||
- 操作风险:`non_destructive`、`metadata_only`、`replace_media`、`delete`、`batch_layout`。
|
||
- 结果落点:右侧追加、填充 slot、当前视口中心、多结果网格、覆盖前确认。
|
||
- 版本关系:source node、revision edge、compare source、生成参数、任务 ID。
|
||
- 区域坐标:局部编辑和框选抠图必须使用图片归一化坐标,不使用屏幕像素。
|
||
- 多结果:图层拆分、三版变体等必须支持 `mediaItems` 或多条 operation,不能只支持单个 `media`。
|
||
|
||
换句话说,Agent 模型可以决定“做什么”,但执行器必须知道“这类动作在现有画布里到底会不会改原节点”。这是防止误改画布的底线。
|
||
|
||
---
|
||
|
||
## 4. 总体架构
|
||
|
||
### 4.1 升级前
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["CanvasChatPanel"] --> B["contextPayload"]
|
||
B --> C["Chat API"]
|
||
C --> D["Agent Graph"]
|
||
D --> E["Image / Video Executor"]
|
||
E --> F["Assistant Message Media"]
|
||
F --> G["前端同步回画布"]
|
||
```
|
||
|
||
问题:写回画布依赖结果消息与前端约定,Agent 不能明确声明“我要填充哪个 slot / 放到哪个锚点旁边 / 形成哪个版本链”。
|
||
|
||
### 4.2 升级后
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["CanvasWorkspace / CanvasChatPanel"] --> B["Canvas Agent Context"]
|
||
B --> C["Chat API"]
|
||
C --> D["Agent Graph"]
|
||
D --> E["Direct Agent Executor"]
|
||
E --> F["Canvas Operation Plan"]
|
||
F --> G["Canvas Mutation API"]
|
||
G --> H["CanvasProject.canvasData"]
|
||
H --> I["CanvasWorkspace Revalidate"]
|
||
```
|
||
|
||
关键变化:
|
||
|
||
- `contextPayload` 升级为可扩展的 `CanvasAgentContext`。
|
||
- Agent 产出 `CanvasOperationPlan`。
|
||
- 执行器调用服务端 mutation,而不是只靠前端消息媒体同步。
|
||
- 画布写入形成明确 metadata 与边关系。
|
||
|
||
### 4.3 对话框驱动画布执行链路
|
||
|
||
对话框驱动画布的完整闭环如下:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant C as CanvasChatPanel
|
||
participant A as Chat API / Agent
|
||
participant E as Executor
|
||
participant M as Canvas Mutation API
|
||
participant W as CanvasWorkspace
|
||
|
||
U->>C: 输入“把选中图右边再生成3个版本”
|
||
C->>A: message + CanvasAgentContext
|
||
A->>A: 解析目标图与画布意图
|
||
A->>E: 执行图片生成或编辑
|
||
E->>A: 返回生成媒体与 CanvasOperationPlan
|
||
A->>M: applyCanvasOperationPlan
|
||
M->>M: 校验权限、计算位置、写入节点与边
|
||
M->>A: 返回 createdNodeIds / canvasData
|
||
A->>C: stream data-canvasOperation
|
||
A->>C: stream data-canvasViewOperation
|
||
C->>W: revalidate 或 patch 本地画布
|
||
C->>W: 聚焦 / 缩放 / 平移视角
|
||
W->>U: 展示新节点、版本连线与正确视角
|
||
```
|
||
|
||
关键原则:
|
||
|
||
- Agent 只产出意图明确的 `CanvasOperationPlan`。
|
||
- Canvas Mutation API 负责真正修改画布,避免 Agent 任意写 JSON。
|
||
- Agent 视角控制通过 `CanvasViewOperationPlan` 在前端执行,默认作为临时 UI 状态,不触发项目保存。
|
||
- 高风险操作必须经过确认门。
|
||
- 默认不破坏原图、原节点、原批注。
|
||
|
||
### 4.4 V2 必修策略
|
||
|
||
为了让本文档从方向方案变成可拆 PR 的执行文档,V2 必须先补齐五个工程边界。
|
||
|
||
1. 新旧 `canvasMedia` 迁移策略
|
||
|
||
当前 Chat API 已经会 stream `data-canvasMedia`,前端 `CanvasChatPanel.appendStreamMediaToCanvas()` 会自动把消息媒体追加到画布。如果同时引入 `CanvasOperationPlan -> Mutation API`,会出现同一张结果图被插入两次的问题。
|
||
|
||
迁移策略:
|
||
|
||
- 阶段 A:保留旧 `data-canvasMedia`,新增 `canvasOperationPlan` shadow log,但不执行 mutation。
|
||
- 阶段 B:将 `canvasAgentWritebackMode` 切到 `operation_authoritative`,由 mutation API 成为权威写回路径。
|
||
- 阶段 C:当某个结果已经通过 `data-canvasOperation` 写回成功,前端必须忽略同批次 `data-canvasMedia`。
|
||
- 阶段 D:验证稳定后,Agent 模式下默认不再为同一结果触发旧 `appendStreamMediaToCanvas()`。
|
||
|
||
协议要求:
|
||
|
||
- 每次 Agent 画布写回必须带 `operationId`。
|
||
- 每个媒体结果必须带稳定 `mediaItemId`。
|
||
- `data-canvasMedia` 与 `data-canvasOperation` 如果描述同一媒体,必须共享 `mediaItemId`。
|
||
- 前端维护 `handledCanvasMediaItemIds`,防止重复插入。
|
||
- 后端返回 `createdNodeIdsByMediaItemId`,用于前端建立“媒体结果 -> 画布节点”的映射。
|
||
|
||
2. autosave 冲突策略
|
||
|
||
当前画布保存是整体 `canvasData` PUT。Agent mutation 是服务端直接改 `canvasProject.canvasData`。如果前端仍持有旧快照并触发 autosave,可能把 mutation API 新插入的节点覆盖掉。
|
||
|
||
迁移策略:
|
||
|
||
- mutation API 必须基于数据库最新 `canvasData` 应用 operation。
|
||
- mutation 请求带 `baseCanvasRevision` 或 `baseCanvasUpdatedAt`。
|
||
- mutation 响应返回新的 `canvasRevision`、`canvasUpdatedAt`、`createdNodeIds`、`updatedNodeIds`。
|
||
- 前端收到 `data-canvasOperation` 后立即 revalidate 或 patch 本地 store。
|
||
- 在 revalidate/patch 完成前,暂停当前画布 autosave。
|
||
- autosave 请求也必须携带 `baseCanvasRevision`。
|
||
- 如果 autosave 的 base revision 落后,服务端拒绝或要求前端重新拉取,不能用旧整体快照覆盖新画布。
|
||
|
||
落地约束:
|
||
|
||
- 第一版 mutation 只做 append-like 操作:`fill_slot`、`insert_beside`、`append_revision`、`link_nodes`,减少 merge 难度。
|
||
- `replace_node_media`、`delete_node`、`move_nodes` 后置,因为这些更容易和用户本地编辑冲突。
|
||
- `syncCanvasNodesForProject()` 只能在合并后的最新 `canvasData` 上执行,不能拿前端旧快照执行。
|
||
|
||
3. slot-only context 策略
|
||
|
||
当前 `buildContextPayload()` 偏图片上下文,若没有有效图片节点容易返回空。Agent Slot 上线后,用户可能只选中一个空 slot,然后说“在这里生成一张图”。这时没有图片,但上下文仍然有效。
|
||
|
||
要求:
|
||
|
||
- `CanvasAgentContext` 可以在没有 `canvasImageNodes` 的情况下发送。
|
||
- 只要存在 selected slot、selected text/ui/video node、annotations、viewport intent 或明确视角指令,就不能因为“没有图片”丢弃上下文。
|
||
- legacy `canvasImageNodes` 可以为空数组,但 `canvasAgentContext.selectedNodes`、`slots`、`viewport` 必须保留。
|
||
- 后端 normalize 不能把“无媒体”误判为“无画布上下文”。
|
||
- 澄清门必须把“选中空 slot + 生成意图”视为可执行,不要求用户再上传图片。
|
||
|
||
4. 视角持久化边界
|
||
|
||
协议层的 `CanvasViewOperationPlan` 是当前用户的临时 UI 指令。它不应该因为 Agent 自动聚焦而把项目默认视角永久改掉。
|
||
|
||
但现有 `CanvasState` 中包含 `viewport`,并且项目保存时可能把 viewport 一起写入 `canvasData`。因此实现时要明确区分:
|
||
|
||
- 用户手动缩放/拖动画布:可以沿用现有保存行为。
|
||
- Agent 自动聚焦/缩放/平移:默认 transient,不进入 autosave。
|
||
- 若未来要保存“演示视角”或“项目默认视角”,需要独立字段与明确用户动作。
|
||
|
||
实现建议:
|
||
|
||
- 增加 `setTransientViewport()` 或 `applyViewOperation({ persist: false })`。
|
||
- autosave builder 跳过最近一次 Agent transient viewport,或记录 `viewportSource = "agent-transient"` 并过滤。
|
||
- `data-canvasViewOperation` 不调用 mutation API。
|
||
- 生成结果节点本身要保存,视角跳转不保存。
|
||
|
||
5. 批量媒体协议
|
||
|
||
现有草案的 `media` 只能表达单个结果,但实际需求包含“再出三版”“图层拆分”“批量改三张图”“多角度生成”等多结果场景。
|
||
|
||
要求:
|
||
|
||
- `CanvasOperationPlan` 支持 `mediaItems`。
|
||
- 单图 `media` 仅作为兼容字段,normalize 后统一转成 `mediaItems`。
|
||
- 每个 `mediaItem` 必须有 `mediaItemId`、`type`、`url`、可选尺寸、标题、来源节点、元数据。
|
||
- 多结果布局必须声明 `placementStrategy`,例如 `row`、`grid`、`overlay`、`beside_each_source`。
|
||
- mutation 结果必须返回每个 `mediaItemId` 对应的 `createdNodeId`。
|
||
|
||
---
|
||
|
||
## 5. 数据协议设计
|
||
|
||
### 5.1 新增类型文件
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
types/canvas/agent-protocol.ts
|
||
```
|
||
|
||
### 5.2 CanvasAgentContext
|
||
|
||
```ts
|
||
export interface CanvasAgentContext {
|
||
version: 1;
|
||
source: "canvas" | "realtime-canvas";
|
||
projectId?: string;
|
||
canvasRevision?: string;
|
||
canvasUpdatedAt?: string;
|
||
viewport?: CanvasViewportContext;
|
||
selectedNodeIds: string[];
|
||
selectedNodes: CanvasAgentNodeRef[];
|
||
candidateNodes: CanvasAgentNodeRef[];
|
||
slots: CanvasAgentSlotRef[];
|
||
annotations: CanvasAgentAnnotationRef[];
|
||
focusPolicy: "auto" | "selected" | "latest_result" | "original" | "slot";
|
||
}
|
||
```
|
||
|
||
`CanvasAgentContext` 不能依赖图片节点是否存在。slot-only、text-only、view-only 请求都必须能形成有效 context。
|
||
|
||
有效 context 判断:
|
||
|
||
- 有 selected image/video/text/ui/workflow/model3d/world3d/agent-slot 节点。
|
||
- 有 selected 或 visible slot。
|
||
- 有 annotations。
|
||
- 用户请求是视角操作,例如“放大选中的图”“缩小看全局”。
|
||
- 有 legacy `canvasImageNodes`。
|
||
|
||
只有以上全部不存在时,才返回 `undefined`。
|
||
|
||
### 5.3 CanvasAgentNodeRef
|
||
|
||
```ts
|
||
export interface CanvasAgentNodeRef {
|
||
nodeId: string;
|
||
type: "image" | "video" | "text" | "ui-screen" | "workflow" | "model3d" | "world3d" | "agent-slot";
|
||
assetId?: string;
|
||
url?: string;
|
||
title?: string;
|
||
prompt?: string;
|
||
x: number;
|
||
y: number;
|
||
width?: number;
|
||
height?: number;
|
||
isSelected?: boolean;
|
||
metadata?: {
|
||
sourceNodeId?: string;
|
||
revisionOf?: string;
|
||
generatedByAgent?: boolean;
|
||
model?: string;
|
||
taskId?: string;
|
||
role?: string;
|
||
};
|
||
}
|
||
```
|
||
|
||
### 5.4 CanvasAgentSlotRef
|
||
|
||
```ts
|
||
export interface CanvasAgentSlotRef {
|
||
nodeId: string;
|
||
slotType: "image" | "video" | "ui-screen";
|
||
label?: string;
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
aspectRatio: string;
|
||
isSelected?: boolean;
|
||
fillPolicy: "replace-empty" | "append-version" | "replace-existing";
|
||
acceptedSources?: Array<"text" | "image" | "selection" | "annotation">;
|
||
}
|
||
```
|
||
|
||
### 5.5 CanvasAgentAnnotationRef
|
||
|
||
```ts
|
||
export interface CanvasAgentAnnotationRef {
|
||
id: string;
|
||
sourceNodeId: string;
|
||
type: "text-note" | "arrow-note" | "box" | "freehand" | "crop-box";
|
||
text?: string;
|
||
bounds?: {
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
};
|
||
targetPoint?: {
|
||
x: number;
|
||
y: number;
|
||
};
|
||
color?: string;
|
||
createdAt?: string;
|
||
}
|
||
```
|
||
|
||
### 5.6 CanvasOperationPlan
|
||
|
||
```ts
|
||
export type CanvasOperationKind =
|
||
| "create_text_node"
|
||
| "fill_slot"
|
||
| "insert_beside"
|
||
| "append_revision"
|
||
| "replace_node_media"
|
||
| "add_annotation"
|
||
| "link_nodes"
|
||
| "move_nodes"
|
||
| "rename_node"
|
||
| "update_note"
|
||
| "update_text";
|
||
|
||
export interface CanvasMediaItem {
|
||
mediaItemId: string;
|
||
type: "image" | "video";
|
||
url: string;
|
||
assetId?: string;
|
||
width?: number;
|
||
height?: number;
|
||
title?: string;
|
||
prompt?: string;
|
||
sourceNodeIds?: string[];
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface CanvasOperationPlan {
|
||
version: 1;
|
||
operationId: string;
|
||
operation: CanvasOperationKind;
|
||
projectId: string;
|
||
baseCanvasRevision?: string;
|
||
baseCanvasUpdatedAt?: string;
|
||
sourceNodeIds?: string[];
|
||
targetSlotId?: string;
|
||
anchorNodeId?: string;
|
||
placement?: "right" | "left" | "below" | "above" | "viewport-center";
|
||
placementStrategy?: "single" | "row" | "grid" | "overlay" | "beside_each_source";
|
||
preserveOriginal?: boolean;
|
||
// 兼容旧调用;后端 normalize 时统一转成 mediaItems。
|
||
media?: {
|
||
type: "image" | "video";
|
||
url: string;
|
||
assetId?: string;
|
||
width?: number;
|
||
height?: number;
|
||
};
|
||
mediaItems?: CanvasMediaItem[];
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
```
|
||
|
||
批量结果规则:
|
||
|
||
- 单媒体:允许传 `media` 或长度为 1 的 `mediaItems`。
|
||
- 多媒体:必须传 `mediaItems`。
|
||
- `mediaItems[].mediaItemId` 必须稳定,供 `data-canvasMedia` 去重和 operation result 映射。
|
||
- “再出三版”使用一个 operation + 三个 `mediaItems`,或三条 operation;首期推荐一个 operation,方便统一布局。
|
||
- “图层拆分”使用 `placementStrategy = "overlay"`,并在每个 `mediaItem.metadata` 中记录 layer role/order。
|
||
- “批量处理三张源图”使用 `placementStrategy = "beside_each_source"`,每个 `mediaItem.sourceNodeIds` 绑定自己的源图。
|
||
|
||
### 5.7 CanvasViewOperationPlan
|
||
|
||
视角控制不应该混进 `CanvasOperationPlan`。Agent 发出的视角操作默认不修改 `canvasProject.canvasData`,只改变当前用户客户端的 viewport。
|
||
|
||
建议新增:
|
||
|
||
```ts
|
||
export type CanvasViewOperationKind =
|
||
| "focus_node"
|
||
| "focus_nodes"
|
||
| "focus_selection"
|
||
| "focus_latest_result"
|
||
| "fit_all"
|
||
| "zoom_in"
|
||
| "zoom_out"
|
||
| "zoom_to"
|
||
| "pan_to";
|
||
|
||
export interface CanvasViewOperationPlan {
|
||
version: 1;
|
||
viewOperationId: string;
|
||
operation: CanvasViewOperationKind;
|
||
projectId?: string;
|
||
nodeIds?: string[];
|
||
anchorNodeId?: string;
|
||
zoom?: number;
|
||
padding?: number;
|
||
durationMs?: number;
|
||
persist?: false;
|
||
center?: {
|
||
x: number;
|
||
y: number;
|
||
};
|
||
reason?: string;
|
||
}
|
||
```
|
||
|
||
设计原则:
|
||
|
||
- `focus_node`:聚焦单个节点,适合“放大这张图继续编辑”。
|
||
- `focus_nodes`:缩放到多个节点的包围盒,适合“看这几张对比图”。
|
||
- `focus_selection`:聚焦当前选区。
|
||
- `focus_latest_result`:生成完成后自动聚焦新结果。
|
||
- `fit_all`:缩小到画布主要内容范围,适合“看全局”。
|
||
- `zoom_in` / `zoom_out` / `zoom_to`:只调缩放。
|
||
- `pan_to`:平移到画布坐标或节点附近。
|
||
- “把选中的图移动到视野中央 / 居中显示当前对象”必须归类为视角操作,不是 `move_nodes`;只有整理、排列、把节点放到画布某位置这类会改变节点坐标的请求才进入移动确认门。
|
||
|
||
持久化边界:
|
||
|
||
- `persist` 首期固定为 `false`。
|
||
- 前端执行 view operation 时需要标记 `viewportSource = "agent-transient"`。
|
||
- autosave 不能因为 Agent transient viewport 变化而触发保存。
|
||
- 用户随后手动缩放/拖动画布时,可以恢复现有 viewport 保存行为。
|
||
|
||
第一期推荐只实现:
|
||
|
||
- `focus_node`
|
||
- `focus_selection`
|
||
- `focus_latest_result`
|
||
- `fit_all`
|
||
- `zoom_in`
|
||
- `zoom_out`
|
||
|
||
### 5.8 首期受控操作白名单
|
||
|
||
Agent 首期只能请求以下操作,后端 mutation 层也只接受这些操作:
|
||
|
||
| 操作 | 用途 | 默认风险等级 | 是否需要确认 |
|
||
| --- | --- | --- | --- |
|
||
| `fill_slot` | 填充选中的图片/视频/UI 槽位 | 低 | 否 |
|
||
| `insert_beside` | 在锚点旁边插入新结果 | 低 | 否 |
|
||
| `append_revision` | 基于源节点追加一个新版本 | 低 | 否 |
|
||
| `link_nodes` | 给两个节点建立参考/版本关系 | 低 | 否 |
|
||
| `create_text_node` | 在当前视口或指定位置创建文本节点 | 低 | 否 |
|
||
| `rename_node` | 修改图片/视频/slot/节点标题 | 低 | 否 |
|
||
| `update_note` | 更新图片/视频节点备注并同步结构化备注标注 | 低 | 否 |
|
||
| `update_text` | 修改选中文本节点内容 | 中 | 视情况 |
|
||
| `replace_node_media` | 替换已有图片/视频内容 | 高 | 是 |
|
||
| `move_nodes` | 批量移动或整理节点 | 中 | 视情况 |
|
||
|
||
第一版正式实现时,建议只开放:
|
||
|
||
- `fill_slot`
|
||
- `insert_beside`
|
||
- `append_revision`
|
||
- `link_nodes`
|
||
- `create_text_node`
|
||
- `rename_node`
|
||
- `update_note`
|
||
|
||
这样可以先把“对话框驱动画布”跑通,同时把误操作风险降到最低。
|
||
|
||
视角操作第一版同步开放:
|
||
|
||
- `focus_node`
|
||
- `focus_selection`
|
||
- `focus_latest_result`
|
||
- `fit_all`
|
||
- `zoom_in`
|
||
- `zoom_out`
|
||
|
||
这类操作不修改画布数据,风险较低,可以作为对话框 Agent 体验的第一批增强能力。
|
||
|
||
---
|
||
|
||
## 6. 画布节点升级
|
||
|
||
### 6.1 新增 NodeType
|
||
|
||
修改:
|
||
|
||
```text
|
||
types/canvas/index.ts
|
||
```
|
||
|
||
新增:
|
||
|
||
```ts
|
||
export type NodeType =
|
||
| "message"
|
||
| "image"
|
||
| "video"
|
||
| "text"
|
||
| "ui-screen"
|
||
| "workflow"
|
||
| "scene-fusion"
|
||
| "model3d"
|
||
| "world3d"
|
||
| "agent-slot";
|
||
```
|
||
|
||
### 6.2 新增 AgentSlotNodeData
|
||
|
||
```ts
|
||
export interface AgentSlotNodeData {
|
||
slotType: "image" | "video" | "ui-screen";
|
||
label?: string;
|
||
status: "empty" | "generating" | "completed" | "failed";
|
||
acceptedSources?: Array<"text" | "image" | "selection" | "annotation">;
|
||
fillPolicy?: "replace-empty" | "append-version" | "replace-existing";
|
||
filledNodeIds?: string[];
|
||
metadata?: Record<string, any>;
|
||
}
|
||
```
|
||
|
||
### 6.3 slot 的行为
|
||
|
||
空 slot:
|
||
|
||
- 显示虚线框。
|
||
- 显示图片/视频/UI 图标。
|
||
- 可调整大小。
|
||
- 可被选中。
|
||
- 可作为 Agent 输出目标。
|
||
|
||
填充后:
|
||
|
||
- 默认不删除 slot。
|
||
- 生成结果作为 slot 子结果或相邻结果。
|
||
- `filledNodeIds` 记录生成节点。
|
||
- 如果 `fillPolicy = replace-existing`,才替换已有结果。
|
||
|
||
---
|
||
|
||
## 7. 前端改造
|
||
|
||
### 7.1 CanvasChatPanel context builder 升级
|
||
|
||
当前位置:
|
||
|
||
```text
|
||
components/canvas/CanvasChatPanel.tsx
|
||
```
|
||
|
||
现有 `buildContextPayload()` 已经输出:
|
||
|
||
- `source`
|
||
- `projectId`
|
||
- `selectedNodeIds`
|
||
- `selectedAssetIds`
|
||
- `focusPolicy`
|
||
- `canvasImageNodes`
|
||
|
||
升级为:
|
||
|
||
```ts
|
||
function buildCanvasAgentContext(): CanvasAgentContext | undefined
|
||
```
|
||
|
||
输出:
|
||
|
||
- selected nodes
|
||
- candidate image/video/ui nodes
|
||
- selected slots
|
||
- visible viewport
|
||
- annotation summaries
|
||
- focus policy
|
||
|
||
兼容期内同时保留旧字段:
|
||
|
||
```ts
|
||
return {
|
||
...legacyContextPayload,
|
||
canvasAgentContext,
|
||
};
|
||
```
|
||
|
||
slot-only / view-only 修正:
|
||
|
||
- 不能继续沿用“没有 `canvasImageNodes` 就返回 `undefined`”的判断。
|
||
- `canvasImageNodes` 只代表 legacy 图片输入,不代表完整画布上下文。
|
||
- 选中空 slot 时,`legacyContextPayload.canvasImageNodes = []`,但 `canvasAgentContext.slots` 和 `selectedNodes` 必须存在。
|
||
- 用户只说“放大选中的图 / 缩小看全局”时,即使不涉及媒体生成,也要发送 viewport 和 selection。
|
||
- 后端 `normalizeCanvasAgentContext()` 必须优先读取 `canvasAgentContext`,再 fallback 到 legacy `canvasImageNodes`。
|
||
|
||
推荐判断:
|
||
|
||
```ts
|
||
const hasAgentContext =
|
||
selectedNodes.length > 0 ||
|
||
slots.length > 0 ||
|
||
annotations.length > 0 ||
|
||
Boolean(viewport) ||
|
||
legacyCanvasImageNodes.length > 0;
|
||
|
||
return hasAgentContext ? contextPayload : undefined;
|
||
```
|
||
|
||
### 7.2 schema 兼容
|
||
|
||
修改:
|
||
|
||
```text
|
||
app/(chat)/api/chat/schema.ts
|
||
```
|
||
|
||
在 `contextPayloadSchema` 中新增:
|
||
|
||
```ts
|
||
canvasAgentContext: z.unknown().optional()
|
||
```
|
||
|
||
第一期不强校验复杂结构,先在后端做 normalize,避免前后端联调成本过高。第二期再补完整 zod schema。
|
||
|
||
### 7.3 新增 AgentSlotNode 组件
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
components/canvas/nodes/AgentSlotNode.tsx
|
||
```
|
||
|
||
最低要求:
|
||
|
||
- 空状态展示。
|
||
- 支持选中态。
|
||
- 支持 resize。
|
||
- 支持右键菜单:
|
||
- “填充此槽位”
|
||
- “设为图片槽”
|
||
- “设为视频槽”
|
||
- “允许替换 / 保留版本”
|
||
- “填充此槽位”必须同时打开/切回右侧对话面板;若对话面板 lazy 未挂载,需要缓存一次 pending slot fill request,挂载后自动预填槽位生成 prompt。
|
||
|
||
### 7.4 工具栏新增 slot 工具
|
||
|
||
修改:
|
||
|
||
```text
|
||
components/canvas/CanvasWorkspace.tsx
|
||
app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
|
||
```
|
||
|
||
新增工具:
|
||
|
||
- `agent-slot-image`
|
||
- `agent-slot-video`
|
||
|
||
用户点击后在当前视口中心创建 slot。
|
||
|
||
### 7.5 画布视角控制执行器
|
||
|
||
当前位置已有基础能力:
|
||
|
||
- `store/canvas/index.ts` 中已有 `setViewport()`、`zoomIn()`、`zoomOut()`、`zoomTo()`、`fitToScreen()`、`centerNode()`。
|
||
- `components/canvas/CanvasChatPanel.tsx` 中已有 `focusCanvasOnNode()`,可作为视觉聚焦的起点。
|
||
|
||
建议新增前端 helper:
|
||
|
||
```text
|
||
lib/canvas/view-operation.ts
|
||
```
|
||
|
||
职责:
|
||
|
||
- 根据 `CanvasViewOperationPlan` 计算新的 viewport。
|
||
- 支持按单节点或多节点 bounding box 聚焦。
|
||
- 支持 padding,避免图片贴边。
|
||
- 支持动画过渡,避免视角跳变。
|
||
|
||
建议函数:
|
||
|
||
```ts
|
||
export function resolveCanvasViewOperation(params: {
|
||
operationPlan: CanvasViewOperationPlan;
|
||
nodes: CanvasNode[];
|
||
viewport: Viewport;
|
||
containerSize: { width: number; height: number };
|
||
}): Viewport | null;
|
||
```
|
||
|
||
前端收到 `data-canvasViewOperation` 后:
|
||
|
||
1. 根据 `nodeIds` 找到节点。
|
||
2. 计算包围盒。
|
||
3. 调用 `setViewport()`。
|
||
4. 必要时同步 `selectNodes()`,让用户知道 Agent 聚焦的是谁。
|
||
|
||
Agent 视角控制默认不保存到数据库,除非后续要做“共享演示视角”。
|
||
|
||
当前项目里 `viewport` 属于 `CanvasState`,可能随 `buildPersistedCanvasData()` 被保存。因此本次改造不能直接复用普通 `setViewport()` 后让 autosave 自然触发。建议:
|
||
|
||
- store 增加 `setTransientViewport(viewport)`,内部更新视图但标记 `viewportSource = "agent-transient"`。
|
||
- autosave 判断如果最近变更只有 `agent-transient` viewport,则跳过保存。
|
||
- 用户手动 wheel zoom、拖动画布、点击缩放按钮后,恢复 `viewportSource = "user"`。
|
||
- `focusCanvasOnNode()` 可复用计算逻辑,但执行 Agent view operation 时必须走 transient 通道。
|
||
|
||
---
|
||
|
||
## 8. 后端改造
|
||
|
||
### 8.1 新增 normalize 模块
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
lib/canvas/agent-context.ts
|
||
```
|
||
|
||
职责:
|
||
|
||
- 从旧 `contextPayload` 和新 `canvasAgentContext` 中归一化上下文。
|
||
- 兼容没有新字段的前端。
|
||
- 支持没有图片媒体的 slot-only / view-only context。
|
||
- 限制节点数量,防止请求过大。
|
||
- 提供 helper:
|
||
- `getSelectedSlots()`
|
||
- `getSelectedMediaNodes()`
|
||
- `getBestAnchorNode()`
|
||
- `getCanvasAgentFocusPolicy()`
|
||
|
||
normalize 优先级:
|
||
|
||
1. 如果存在 `contextPayload.canvasAgentContext`,优先使用它。
|
||
2. 如果没有新 context,再从 legacy `canvasImageNodes` 构造 `candidateNodes`。
|
||
3. 如果新 context 存在但 `canvasImageNodes` 为空,不允许丢弃 context。
|
||
4. `selectedSlotCount > 0` 时,媒体输入为空也可以继续规划生成任务。
|
||
|
||
### 8.2 新增 mutation 服务
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
lib/canvas/agent-mutations.ts
|
||
```
|
||
|
||
核心函数:
|
||
|
||
```ts
|
||
export async function applyCanvasOperationPlan(params: {
|
||
userId: string;
|
||
plan: CanvasOperationPlan;
|
||
}): Promise<CanvasOperationResult>
|
||
```
|
||
|
||
内部流程:
|
||
|
||
1. 读取 `canvasProject`。
|
||
2. 校验 userId 权限。
|
||
3. 校验 `baseCanvasRevision` / `baseCanvasUpdatedAt`。
|
||
4. 如 revision 落后,拒绝高风险操作;append-like 操作可在最新画布上重新计算位置后继续。
|
||
5. 校验 source/slot/anchor 节点存在。
|
||
6. normalize `media` / `mediaItems`。
|
||
7. 计算插入位置。
|
||
8. 创建或更新节点。
|
||
9. 创建 edge。
|
||
10. 写入 metadata。
|
||
11. 调用 `syncCanvasNodesForProject()`。
|
||
12. 保存 `canvasProject.canvasData` 并生成新 `canvasRevision`。
|
||
|
||
autosave 冲突要求:
|
||
|
||
- mutation 服务永远基于数据库最新 `canvasData` 修改。
|
||
- 不接受前端传入的整份旧 `canvasData` 作为 mutation 基础。
|
||
- mutation 结果必须返回最新 revision,前端据此刷新本地 store。
|
||
- 如果同一 `operationId` 重试,服务端必须幂等,不重复创建节点。
|
||
- 服务端可存储或从 metadata 中查找 `operationId`,避免网络重试导致重复写入。
|
||
|
||
首期允许在 revision 不一致时自动合并的操作:
|
||
|
||
- `insert_beside`
|
||
- `fill_slot`,仅限空 slot 或 append-version slot
|
||
- `append_revision`
|
||
- `link_nodes`
|
||
- `rename_node`
|
||
- `update_note`
|
||
|
||
首期 revision 不一致时必须拒绝或要求确认的操作:
|
||
|
||
- `replace_node_media`
|
||
- `delete_node`
|
||
- `move_nodes`
|
||
- `replace-existing` slot fill
|
||
|
||
### 8.3 新增 API
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
app/api/canvas/[id]/agent-operations/route.ts
|
||
```
|
||
|
||
接口:
|
||
|
||
```http
|
||
POST /api/canvas/:id/agent-operations
|
||
```
|
||
|
||
请求:
|
||
|
||
```json
|
||
{
|
||
"operationPlan": {
|
||
"version": 1,
|
||
"operationId": "op_...",
|
||
"operation": "append_revision",
|
||
"projectId": "...",
|
||
"baseCanvasRevision": "...",
|
||
"sourceNodeIds": ["..."],
|
||
"anchorNodeId": "...",
|
||
"placement": "right",
|
||
"preserveOriginal": true,
|
||
"mediaItems": [
|
||
{
|
||
"mediaItemId": "media_...",
|
||
"type": "image",
|
||
"url": "https://..."
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"createdNodeIds": ["..."],
|
||
"createdNodeIdsByMediaItemId": {
|
||
"media-1": "node-1"
|
||
},
|
||
"updatedNodeIds": [],
|
||
"createdEdgeIds": ["..."],
|
||
"canvasData": {},
|
||
"canvasRevision": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Agent 编排改造
|
||
|
||
### 9.1 AutoAgentInput 扩展
|
||
|
||
修改:
|
||
|
||
```text
|
||
lib/ai/auto-agent-graph.ts
|
||
```
|
||
|
||
新增:
|
||
|
||
```ts
|
||
canvasAgentContext?: CanvasAgentContext | null;
|
||
selectedSlotCount?: number;
|
||
hasSelectedAgentSlot?: boolean;
|
||
```
|
||
|
||
规划提示词中新增规则:
|
||
|
||
- 如果存在选中的空 slot,优先 `fill_slot`。
|
||
- 如果是编辑选中图,默认 `append_revision`。
|
||
- 如果是生成新图且无 slot,默认 `insert_beside` 或 `viewport-center`。
|
||
- 如果用户说“整理 / 排列 / 放到右边 / 连起来”,先输出低风险布局或连线计划,不直接改媒体内容。
|
||
- 只有用户明确说“替换 / 覆盖 / 删除原来的”,才使用 `replace_node_media`。
|
||
|
||
### 9.1.1 对话指令到画布操作的映射
|
||
|
||
| 用户表达 | 推荐操作 | 目标解析 |
|
||
| --- | --- | --- |
|
||
| “在这个空框里生成一张图” | `fill_slot` | selected slot |
|
||
| “把选中图片背景换成白色” | `append_revision` | selected image |
|
||
| “再出三版放右边” | `insert_beside` | selected image 或 latest result |
|
||
| “按批注修改” | `append_revision` | selected image + annotations |
|
||
| “把这两张连起来” | `link_nodes` | selected nodes |
|
||
| “把选中图片改名为白底主图 A” | `rename_node` | selected image/video/slot node |
|
||
| “给选中图片加备注:保留主体轮廓” | `update_note` | selected image/video node |
|
||
| “在画布上添加文字:新品上市” | `create_text_node` | current viewport center 或 targetPosition |
|
||
| “把这段文字改成更高级” | `update_text` | selected text node |
|
||
| “替换掉原图” | `replace_node_media` | selected image,需要确认 |
|
||
| “把这些整理成三列” | `move_nodes` | selected nodes,建议确认 |
|
||
| “放大选中的图” | `focus_selection` | selected nodes |
|
||
| “聚焦刚生成的结果” | `focus_latest_result` | latest result |
|
||
| “缩小看全局” | `fit_all` | all visible nodes |
|
||
| “对比原图和新图” | `focus_nodes` | source + result nodes |
|
||
|
||
### 9.2 Direct Agent Executor 扩展
|
||
|
||
相关文件:
|
||
|
||
```text
|
||
lib/ai/runtime/direct-agent-execution.ts
|
||
lib/ai/runtime/direct-agent-media-executors.ts
|
||
lib/ai/runtime/direct-agent-image-edit-executor.ts
|
||
```
|
||
|
||
新增输出:
|
||
|
||
```ts
|
||
canvasOperationPlan?: CanvasOperationPlan;
|
||
canvasOperationPlans?: CanvasOperationPlan[];
|
||
canvasViewOperationPlan?: CanvasViewOperationPlan;
|
||
```
|
||
|
||
约束:
|
||
|
||
- 单结果可以返回 `canvasOperationPlan`。
|
||
- 多批次或多目标可以返回 `canvasOperationPlans`。
|
||
- 单个 operation 内的多媒体结果必须放入 `mediaItems`。
|
||
- executor 生成的 assistant message media 必须带同一个 `mediaItemId`,供旧 `data-canvasMedia` 去重。
|
||
|
||
图片生成:
|
||
|
||
- 有 selected slot:`fill_slot`
|
||
- 有 selected image:`insert_beside`
|
||
- 无选中:`insert_beside` + `placement = "viewport-center"`,并在 `metadata.canvasAgentViewport` 写入请求时的 viewport(含 width/height),服务端 mutation 按真实当前视口中心计算落点
|
||
|
||
图片编辑:
|
||
|
||
- 默认 `append_revision`
|
||
- `sourceNodeIds = target refs nodeId`
|
||
- `anchorNodeId = target nodeId`
|
||
- `preserveOriginal = true`
|
||
|
||
视频生成:
|
||
|
||
- 有 video slot:`fill_slot`
|
||
- 有 image anchor:`insert_beside`
|
||
|
||
### 9.3 Chat API 写回
|
||
|
||
修改:
|
||
|
||
```text
|
||
app/(chat)/api/chat/route.ts
|
||
```
|
||
|
||
这是新旧写回迁移的核心位置。当前代码已有旧链路:
|
||
|
||
- Chat API stream `data-canvasMedia`
|
||
- 前端 `CanvasChatPanel` 收到后调用 `appendStreamMediaToCanvas()`
|
||
|
||
V2 新链路是:
|
||
|
||
- executor 返回 `CanvasOperationPlan`
|
||
- Chat API 调用 `/api/canvas/[id]/agent-operations` 或内部 mutation service
|
||
- mutation API 返回 created nodes
|
||
- Chat API stream `data-canvasOperation`
|
||
- 前端刷新/patch 画布,并用 `handledCanvasMediaItemIds` 忽略同批旧媒体
|
||
|
||
在生成/编辑成功后:
|
||
|
||
1. 如果请求来自 canvas。
|
||
2. 如果有 `projectId`。
|
||
3. 如果 executor 返回 `canvasOperationPlan`。
|
||
4. 如果 `canvasAgentWritebackMode = "operation_authoritative"`,调用 `applyCanvasOperationPlan()`。
|
||
5. stream 一个 canvas operation chunk 给前端。
|
||
6. 把 operation 中所有 `mediaItemId` 加入本次响应的 `handledCanvasMediaItemIds`。
|
||
7. 如果后续仍 stream legacy `data-canvasMedia`,必须附带相同 `mediaItemId`,前端据此跳过自动插入。
|
||
8. 如果需要聚焦新结果或当前编辑对象,额外 stream 一个 canvas view operation chunk。
|
||
|
||
迁移开关:
|
||
|
||
```ts
|
||
type CanvasWritebackMode =
|
||
| "legacy_canvas_media"
|
||
| "operation_shadow"
|
||
| "operation_authoritative";
|
||
```
|
||
|
||
- `legacy_canvas_media`:保持现状,只发 `data-canvasMedia`。
|
||
- `operation_shadow`:生成 operation 并记录日志,但不写库,仍走旧前端插入。
|
||
- `operation_authoritative`:mutation API 写库,前端忽略同批 `data-canvasMedia`。
|
||
|
||
建议新增 stream part:
|
||
|
||
```ts
|
||
data-canvasOperation
|
||
```
|
||
|
||
payload:
|
||
|
||
```json
|
||
{
|
||
"projectId": "...",
|
||
"operationId": "op_...",
|
||
"operation": "append_revision",
|
||
"createdNodeIds": ["..."],
|
||
"createdNodeIdsByMediaItemId": {
|
||
"media_...": "node_..."
|
||
},
|
||
"canvasRevision": "...",
|
||
"anchorNodeId": "...",
|
||
"handledCanvasMediaItemIds": ["media_..."],
|
||
"message": "已放到原图右侧"
|
||
}
|
||
```
|
||
|
||
前端收到后:
|
||
|
||
- 暂停 autosave。
|
||
- mutate 当前画布项目或直接 patch 本地 store。
|
||
- 记录新的 `canvasRevision`。
|
||
- 把 `handledCanvasMediaItemIds` 加入本地去重集合。
|
||
- 恢复 autosave。
|
||
- 执行 `CanvasViewOperationPlan`,把视角移动到目标节点或选区,且视角变化不触发保存。
|
||
|
||
第一期建议 mutate 重新拉取,稳定优先。
|
||
|
||
建议新增 stream part:
|
||
|
||
```ts
|
||
data-canvasViewOperation
|
||
```
|
||
|
||
payload:
|
||
|
||
```json
|
||
{
|
||
"viewOperationId": "view_...",
|
||
"operation": "focus_latest_result",
|
||
"projectId": "...",
|
||
"nodeIds": ["..."],
|
||
"zoom": 0.9,
|
||
"padding": 120,
|
||
"persist": false,
|
||
"reason": "聚焦刚生成的新版本"
|
||
}
|
||
```
|
||
|
||
### 9.4 视角控制 Agent 规则
|
||
|
||
Agent 需要识别两类视角请求:
|
||
|
||
1. 单纯视角请求,不触发生成或编辑:
|
||
- “放大选中的图”
|
||
- “缩小一点”
|
||
- “看全局”
|
||
- “聚焦到右边那张”
|
||
2. 执行后的自动视角请求:
|
||
- 生成完成后聚焦新图
|
||
- 编辑完成后展示原图和新版本
|
||
- 多版本生成后缩放到这组结果
|
||
|
||
规则:
|
||
|
||
- 用户只要求视角变化时,不调用生成/编辑 executor。
|
||
- 有选中图时,“放大这张 / 聚焦这个”优先 `focus_selection`。
|
||
- 生成/编辑成功后,默认返回 `focus_latest_result`。
|
||
- 如果用户要求“对比原图和新图”,返回 `focus_nodes`,包含 source 与 result。
|
||
- 如果用户要求“看全部 / 缩小看全局”,返回 `fit_all`。
|
||
- 如果没有选中且目标不明确,只做 `fit_all` 或提示用户先选中。
|
||
|
||
---
|
||
|
||
## 10. 标注编辑最小闭环
|
||
|
||
### 10.1 本期标注模型
|
||
|
||
不做复杂绘图系统,先支持三类:
|
||
|
||
- 文本批注
|
||
- 框选区域
|
||
- 箭头指向
|
||
|
||
可以先存到图片节点 metadata:
|
||
|
||
```ts
|
||
metadata: {
|
||
annotations: CanvasAgentAnnotationRef[]
|
||
}
|
||
```
|
||
|
||
### 10.2 前端入口
|
||
|
||
图片节点右键菜单新增:
|
||
|
||
- “添加批注”
|
||
- “按批注修改”
|
||
|
||
“按批注修改”行为:
|
||
|
||
1. 选中当前图片。
|
||
2. 将 annotations 放入 `CanvasAgentContext`。
|
||
3. 自动在输入框插入一句:“按这张图上的批注修改”。
|
||
4. 用户可直接发送,也可补充文字。
|
||
5. 若右侧对话面板关闭或 lazy 未挂载,需要先展开对话框,并缓存一次 pending annotation edit request,确保 prompt 与目标图不丢。
|
||
|
||
### 10.3 Agent 规则
|
||
|
||
如果目标图存在 annotations 且用户说“按批注 / 按标注 / 按箭头 / 按框选”:
|
||
|
||
- 使用 annotations 作为编辑 brief。
|
||
- 输出 clean revised image。
|
||
- 不把批注文字、箭头、框线生成进新图。
|
||
- 默认 `append_revision`。
|
||
|
||
---
|
||
|
||
## 11. 插入与布局规则
|
||
|
||
### 11.1 insert beside
|
||
|
||
默认:
|
||
|
||
- `placement = right`
|
||
- `margin = 40`
|
||
- 新节点尺寸优先匹配 anchor。
|
||
- 如右侧重叠,继续向右寻找空位。
|
||
- 超出合理距离后改为下方。
|
||
|
||
### 11.2 fill slot
|
||
|
||
默认:
|
||
|
||
- 新媒体节点尺寸匹配 slot。
|
||
- 结果节点与 slot 建立 edge。
|
||
- slot `filledNodeIds` 追加新节点。
|
||
- slot 本身保留。
|
||
|
||
### 11.3 append revision
|
||
|
||
默认:
|
||
|
||
- 新节点放 source 右侧。
|
||
- 创建 edge:
|
||
- `source -> newNode`
|
||
- type: `reference`
|
||
- 新节点 metadata:
|
||
|
||
```ts
|
||
metadata: {
|
||
generatedByAgent: true,
|
||
revisionOf: sourceNodeId,
|
||
sourceNodeId,
|
||
sourceNodeIds: [...],
|
||
prompt,
|
||
model,
|
||
messageId,
|
||
operation: "append_revision"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 澄清策略升级
|
||
|
||
修改:
|
||
|
||
```text
|
||
lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||
```
|
||
|
||
### 12.1 默认不澄清
|
||
|
||
以下场景默认继续执行:
|
||
|
||
- 单选图片 + 明确编辑意图。
|
||
- 单选 slot + 明确生成意图。
|
||
- 无选中 + 明确生成意图。
|
||
- 有 annotations + “按标注修改”。
|
||
- 生成变体、追加版本、放旁边。
|
||
|
||
### 12.2 必须澄清
|
||
|
||
以下场景必须澄清:
|
||
|
||
- 多选图片 + 编辑意图 + 没有“全部/批量/统一”。
|
||
- 用户说“替换/覆盖/删除”,但目标不唯一。
|
||
- 用户只说“就这个/开始吧”,且 action 不明确。
|
||
- 目标解析低置信度,并且会修改已有节点。
|
||
|
||
### 12.3 可执行但提示
|
||
|
||
以下场景可以执行,但回复中提示选择:
|
||
|
||
- 无 slot 的生成:默认放当前画布空白处。
|
||
- 无选中但有唯一候选图:默认处理唯一候选。
|
||
- 目标图来自上一版结果:提示“已按上一版继续”。
|
||
|
||
---
|
||
|
||
## 13. 分阶段实施计划
|
||
|
||
### 阶段 1:协议与上下文,不写回
|
||
|
||
目标:前后端能稳定传输 `CanvasAgentContext`。
|
||
|
||
任务:
|
||
|
||
- 新增 `types/canvas/agent-protocol.ts`
|
||
- 扩展 `contextPayloadSchema`
|
||
- 新增 `lib/canvas/agent-context.ts`
|
||
- 改造 `CanvasChatPanel.buildContextPayload()`
|
||
- 保持旧字段兼容
|
||
- 支持 slot-only / view-only context
|
||
- 新增 `operationId`、`mediaItemId`、`baseCanvasRevision` 类型
|
||
- 增加单元测试:
|
||
- selected image 归一化
|
||
- selected slot 归一化
|
||
- slot-only context 不被丢弃
|
||
- view-only context 不被丢弃
|
||
- annotations 归一化
|
||
- 超量节点裁剪
|
||
|
||
验收:
|
||
|
||
- 现有画布对话不受影响。
|
||
- Chat API 日志能看到 normalized canvas agent context。
|
||
- 选中空 slot 且没有任何图片时,Chat API 仍能收到 selected slot。
|
||
- 用户只发“缩小看全局”时,后端能收到 viewport context。
|
||
- 未升级前端仍可调用 Chat API。
|
||
|
||
### 阶段 2:Agent Slot 前端能力
|
||
|
||
目标:用户能在画布创建和选中输出槽位。
|
||
|
||
任务:
|
||
|
||
- 新增 `AgentSlotNodeData`
|
||
- 新增 `agent-slot` NodeType
|
||
- 新增 `AgentSlotNode.tsx`
|
||
- 工具栏增加图片 slot
|
||
- 右键菜单支持“填充此槽位”
|
||
- 槽位右键填充会展开对话框并在面板未挂载时保留 pending request
|
||
- `buildCanvasAgentContext()` 输出 slots
|
||
|
||
验收:
|
||
|
||
- 创建 slot 后保存/刷新不丢。
|
||
- 选中 slot 后发送“生成一张图”,后端能识别 selected slot。
|
||
- 旧图片/视频节点不受影响。
|
||
|
||
### 阶段 3:Canvas Mutation API
|
||
|
||
目标:后端能安全写回画布。
|
||
|
||
任务:
|
||
|
||
- 新增 `lib/canvas/agent-mutations.ts`
|
||
- 新增 `/api/canvas/[id]/agent-operations`
|
||
- 新增 canvas revision / base revision 校验
|
||
- 新增 operation 幂等处理
|
||
- 实现:
|
||
- `insert_beside`
|
||
- `fill_slot`
|
||
- `append_revision`
|
||
- 写入 metadata 与 edges
|
||
- 返回 `createdNodeIdsByMediaItemId`
|
||
- 调用 `syncCanvasNodesForProject()`
|
||
|
||
验收:
|
||
|
||
- API 能在指定 anchor 右侧插入图片节点。
|
||
- API 能填充 slot 并记录 `filledNodeIds`。
|
||
- API 能创建 revision edge。
|
||
- 同一个 `operationId` 重试不重复创建节点。
|
||
- stale autosave 不能覆盖 mutation 新增节点。
|
||
- 非当前用户无法写入。
|
||
|
||
### 阶段 4:Agent 执行链路接入
|
||
|
||
目标:生成/编辑成功后自动写回画布。
|
||
|
||
任务:
|
||
|
||
- 扩展 direct executor 返回 `canvasOperationPlan` / `canvasOperationPlans`
|
||
- executor media 输出带 `mediaItemId`
|
||
- Chat API 成功后调用 mutation
|
||
- 新增 `CanvasWritebackMode`
|
||
- 先支持 `operation_shadow`,再开启 `operation_authoritative`
|
||
- stream `data-canvasOperation`
|
||
- stream `data-canvasViewOperation`
|
||
- 前端收到后刷新画布,并记录 `handledCanvasMediaItemIds`
|
||
|
||
验收:
|
||
|
||
- 选中 slot 后:“生成一张赛博风主图”能填充 slot。
|
||
- 选中图片后:“换成白底”能在右侧生成新版本。
|
||
- “再出三版”能生成 3 个节点且不重复插入。
|
||
- 旧 `data-canvasMedia` 和新 `data-canvasOperation` 同时存在时,不出现重复图片节点。
|
||
- 原图不被覆盖。
|
||
- 画布出现 source -> result 边。
|
||
- 生成后视角自动聚焦新结果。
|
||
|
||
### 阶段 5:画布视角控制
|
||
|
||
目标:用户能用对话框控制画布缩放、平移和聚焦。
|
||
|
||
任务:
|
||
|
||
- 新增 `CanvasViewOperationPlan`
|
||
- 新增 `lib/canvas/view-operation.ts`
|
||
- 新增 transient viewport 执行通道
|
||
- 前端消费 `data-canvasViewOperation`
|
||
- 支持:
|
||
- `focus_selection`
|
||
- `focus_latest_result`
|
||
- `focus_node`
|
||
- `fit_all`
|
||
- `zoom_in`
|
||
- `zoom_out`
|
||
|
||
验收:
|
||
|
||
- 用户说“放大选中的图”,画布聚焦选中节点。
|
||
- 用户说“缩小看全局”,画布缩放到主要内容范围。
|
||
- 生成新图后,画布自动聚焦新结果。
|
||
- 用户说“对比原图和新图”,画布缩放到两者的共同包围盒。
|
||
- Agent 自动聚焦不触发 autosave,不改项目保存视角。
|
||
|
||
### 阶段 6:标注编辑最小闭环
|
||
|
||
目标:用户能基于画布批注让 Agent 修图。
|
||
|
||
任务:
|
||
|
||
- 图片节点 metadata 支持 annotations
|
||
- 图片节点 UI 支持添加文本批注/框选批注
|
||
- `CanvasAgentContext` 输出 annotations
|
||
- “按批注修改”会展开对话框并在面板未挂载时保留 pending request
|
||
- Agent prompt 加入标注编辑规则
|
||
- 编辑结果使用 `append_revision`
|
||
|
||
验收:
|
||
|
||
- 用户给图片加批注“背景换浅灰”,发送“按批注修改”,能生成新图。
|
||
- 新图不包含批注文字和框线。
|
||
- 原图与批注保留。
|
||
|
||
---
|
||
|
||
## 14. 测试计划
|
||
|
||
### 14.1 单元测试
|
||
|
||
新增或扩展:
|
||
|
||
```text
|
||
tests/routes/canvas-chat-helpers.test.ts
|
||
```
|
||
|
||
覆盖:
|
||
|
||
- canvas agent context normalize
|
||
- selected slot 优先级
|
||
- slot-only context 保留
|
||
- view-only context 保留
|
||
- selected image 优先级
|
||
- annotation brief 提取
|
||
- media normalize:`media` 转 `mediaItems`
|
||
- 批量 `mediaItems` layout strategy
|
||
- `operationId` 幂等 key 生成
|
||
- operation plan placement 计算
|
||
- view operation viewport 计算
|
||
- transient viewport 不进入 persisted canvasData
|
||
- focus selection / fit all / zoom in / zoom out
|
||
- 多选澄清策略
|
||
|
||
### 14.2 API 测试
|
||
|
||
新增:
|
||
|
||
```text
|
||
tests/routes/canvas-agent-operations.test.ts
|
||
```
|
||
|
||
覆盖:
|
||
|
||
- 未登录 401
|
||
- 非拥有者 404/403
|
||
- anchor 不存在 400
|
||
- fill slot 成功
|
||
- append revision 成功
|
||
- 多 `mediaItems` 成功创建多个节点
|
||
- 返回 `createdNodeIdsByMediaItemId`
|
||
- 同 `operationId` 重试幂等
|
||
- stale `baseCanvasRevision` 对高风险操作返回冲突
|
||
- replace 需要显式 operation
|
||
|
||
### 14.3 E2E 手测路径
|
||
|
||
1. 创建画布。
|
||
2. 添加图片。
|
||
3. 选中图片,在 Agent 对话框输入“换成白底”。
|
||
4. 验证右侧出现新图,原图保留。
|
||
5. 创建图片 slot。
|
||
6. 选中 slot,输入“生成一张电商主图”。
|
||
7. 验证结果填入 slot。
|
||
8. 对原图添加批注,输入“按批注修改”。
|
||
9. 验证新图放右侧且不包含批注痕迹。
|
||
10. 选中一张图,输入“放大选中的图”,验证画布聚焦该图。
|
||
11. 输入“缩小看全局”,验证画布缩放到主要内容范围。
|
||
12. 输入“对比原图和新图”,验证画布同时框住两个节点。
|
||
13. 选中空 slot 且没有图片,输入“在这里生成一张主图”,验证不被澄清且结果填入 slot。
|
||
14. 输入“再出三版”,验证只出现 3 个新图,不被旧 `canvasMedia` 重复插入成 6 个。
|
||
15. Agent 生成期间触发一次普通 autosave,验证新生成节点不被旧快照覆盖。
|
||
16. Agent 自动聚焦后刷新页面,验证项目默认视角没有被 Agent transient viewport 改掉。
|
||
|
||
---
|
||
|
||
## 15. 灰度与开关
|
||
|
||
建议新增站点设置:
|
||
|
||
```ts
|
||
canvasAgentOperationProtocolEnabled: boolean
|
||
canvasAgentWritebackMode: "legacy_canvas_media" | "operation_shadow" | "operation_authoritative"
|
||
canvasAgentSlotEnabled: boolean
|
||
canvasAgentViewOperationEnabled: boolean
|
||
canvasAgentAnnotationEditEnabled: boolean
|
||
```
|
||
|
||
灰度顺序:
|
||
|
||
1. 开启 context protocol,只读不写。
|
||
2. 开启 `operation_shadow`,记录 operation 但仍走旧 `canvasMedia`。
|
||
3. 默认开启 slot,但 mutation 写回只对 Agent 模式开启。
|
||
4. 开启 mutation API,仅内部用户。
|
||
5. 切到 `operation_authoritative`,前端按 `mediaItemId` 去重旧 `canvasMedia`。
|
||
6. 开启视角操作能力,只影响当前用户客户端,且不持久化 Agent transient viewport。
|
||
7. 全量开启非破坏式 `insert_beside` / `append_revision`。
|
||
8. 最后开放 `replace_node_media`。
|
||
|
||
---
|
||
|
||
## 16. 风险与规避
|
||
|
||
### 16.1 误改画布
|
||
|
||
规避:
|
||
|
||
- 默认非破坏。
|
||
- 替换/删除必须显式。
|
||
- metadata 记录操作来源。
|
||
|
||
### 16.2 画布状态过大
|
||
|
||
规避:
|
||
|
||
- context 只传最多 12 个 candidate nodes。
|
||
- annotations 最多传 20 条。
|
||
- 后端再次裁剪。
|
||
|
||
### 16.3 前后端协议漂移
|
||
|
||
规避:
|
||
|
||
- `types/canvas/agent-protocol.ts` 单点定义。
|
||
- 后端 normalize 兼容旧字段。
|
||
- 第一阶段不强制完整 zod,第二阶段补。
|
||
|
||
### 16.4 生成结果插入位置不稳定
|
||
|
||
规避:
|
||
|
||
- 统一 placement helper。
|
||
- 默认右侧,冲突则递进。
|
||
- 最终 fallback 到 viewport center。
|
||
|
||
### 16.5 视角跳变影响用户操作
|
||
|
||
规避:
|
||
|
||
- 只有用户明确要求聚焦/缩放,或生成完成后才自动聚焦。
|
||
- 视角操作使用短动画过渡。
|
||
- 用户正在拖拽、框选、编辑文本时不强制切换视角。
|
||
- 提供前端开关 `canvasAgentViewOperationEnabled` 独立灰度。
|
||
|
||
### 16.6 旧 `canvasMedia` 与新 operation 重复插入
|
||
|
||
规避:
|
||
|
||
- 引入 `canvasAgentWritebackMode` 分阶段迁移。
|
||
- `operation_authoritative` 模式下,前端按 `mediaItemId` 忽略同批 legacy media。
|
||
- E2E 必测“再出三版”不会插入 6 张图。
|
||
|
||
### 16.7 autosave 覆盖 Agent mutation
|
||
|
||
规避:
|
||
|
||
- mutation 与 autosave 都携带 base revision。
|
||
- 前端收到 operation 后暂停 autosave 并 revalidate。
|
||
- stale autosave 不允许覆盖数据库最新 `canvasData`。
|
||
|
||
---
|
||
|
||
## 17. 推荐首个 PR 切分
|
||
|
||
### PR 1:协议与 normalize
|
||
|
||
- 新增协议类型
|
||
- 扩展 schema
|
||
- 前端输出 `canvasAgentContext`
|
||
- 修复 slot-only / view-only context
|
||
- 增加 `mediaItems`、`operationId`、`mediaItemId`、`baseCanvasRevision`
|
||
- 后端 normalize
|
||
- 测试 normalize
|
||
|
||
### PR 2:Agent Slot 节点
|
||
|
||
- 新增 NodeType
|
||
- 新增节点组件
|
||
- 工具栏创建 slot
|
||
- 保存/加载验证
|
||
|
||
### PR 3:Canvas Mutation API
|
||
|
||
- 新增 mutation service
|
||
- 新增 API route
|
||
- revision 冲突检测
|
||
- operation 幂等
|
||
- 批量 `mediaItems`
|
||
- 支持 `insert_beside` / `fill_slot` / `append_revision`
|
||
- API 测试
|
||
|
||
### PR 4:新旧写回迁移
|
||
|
||
- 新增 `canvasAgentWritebackMode`
|
||
- 支持 `operation_shadow`
|
||
- legacy `data-canvasMedia` 带 `mediaItemId`
|
||
- 前端 `handledCanvasMediaItemIds` 去重
|
||
- 验证不开重复节点
|
||
|
||
### PR 5:Agent 生成写回
|
||
|
||
- direct executor 生成 operation plan
|
||
- Chat API 调 mutation
|
||
- 前端刷新画布
|
||
- `operation_authoritative` 模式写回
|
||
|
||
### PR 6:画布视角控制
|
||
|
||
- 新增 view operation 类型
|
||
- 新增 view operation resolver
|
||
- Chat API stream `data-canvasViewOperation`
|
||
- 前端执行聚焦、缩放、看全局
|
||
- transient viewport 不触发 autosave
|
||
|
||
### PR 7:标注编辑 MVP
|
||
|
||
- annotation metadata
|
||
- 图片节点批注入口
|
||
- Agent 按批注编辑
|
||
- append revision 验证
|
||
|
||
---
|
||
|
||
## 18. 最小可交付定义
|
||
|
||
第一版上线只要满足:
|
||
|
||
1. 用户能在画布创建一个图片 slot。
|
||
2. 选中 slot 后,在对话框说“生成一张 xxx”,结果能填充 slot。
|
||
3. 选中图片后,说“把背景换成白色”,结果能放在原图右侧。
|
||
4. 原图不被覆盖。
|
||
5. 新图 metadata 能记录 source、prompt、model、messageId。
|
||
6. 刷新页面后版本关系仍在。
|
||
7. 对话框能执行“放大选中的图 / 缩小看全局 / 聚焦刚生成结果”。
|
||
|
||
这 7 点完成,就已经从“结果同步到画布”升级到了“Agent 按画布意图工作”。
|