docs: update canvas agent upgrade plan
This commit is contained in:
parent
038730a24b
commit
c528e22680
@ -1,6 +1,7 @@
|
||||
# 无限画布对话框 Agent 模式升级开发文档
|
||||
|
||||
> 日期:2026-06-22
|
||||
> 当前版本:V2 修订版
|
||||
> 参考项目:Cowart(https://github.com/zhongerxin/cowart)
|
||||
> 目标:把当前“对话框 Agent 能理解画布上下文并生成结果”,升级为“Agent 能通过稳定协议读写画布、填充画布意图、保留版本链、低打扰执行”的可落地系统。
|
||||
|
||||
@ -405,10 +406,94 @@ sequenceDiagram
|
||||
|
||||
- Agent 只产出意图明确的 `CanvasOperationPlan`。
|
||||
- Canvas Mutation API 负责真正修改画布,避免 Agent 任意写 JSON。
|
||||
- 视角控制通过 `CanvasViewOperationPlan` 在前端执行,不写入项目数据。
|
||||
- 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. 数据协议设计
|
||||
@ -428,6 +513,8 @@ export interface CanvasAgentContext {
|
||||
version: 1;
|
||||
source: "canvas" | "realtime-canvas";
|
||||
projectId?: string;
|
||||
canvasRevision?: string;
|
||||
canvasUpdatedAt?: string;
|
||||
viewport?: CanvasViewportContext;
|
||||
selectedNodeIds: string[];
|
||||
selectedNodes: CanvasAgentNodeRef[];
|
||||
@ -438,6 +525,18 @@ export interface CanvasAgentContext {
|
||||
}
|
||||
```
|
||||
|
||||
`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
|
||||
@ -518,15 +617,33 @@ export type CanvasOperationKind =
|
||||
| "move_nodes"
|
||||
| "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;
|
||||
@ -534,13 +651,23 @@ export interface CanvasOperationPlan {
|
||||
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`。它不修改 `canvasProject.canvasData`,只改变当前用户客户端的 viewport。
|
||||
视角控制不应该混进 `CanvasOperationPlan`。Agent 发出的视角操作默认不修改 `canvasProject.canvasData`,只改变当前用户客户端的 viewport。
|
||||
|
||||
建议新增:
|
||||
|
||||
@ -558,6 +685,7 @@ export type CanvasViewOperationKind =
|
||||
|
||||
export interface CanvasViewOperationPlan {
|
||||
version: 1;
|
||||
viewOperationId: string;
|
||||
operation: CanvasViewOperationKind;
|
||||
projectId?: string;
|
||||
nodeIds?: string[];
|
||||
@ -565,6 +693,7 @@ export interface CanvasViewOperationPlan {
|
||||
zoom?: number;
|
||||
padding?: number;
|
||||
durationMs?: number;
|
||||
persist?: false;
|
||||
center?: {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -583,6 +712,13 @@ export interface CanvasViewOperationPlan {
|
||||
- `zoom_in` / `zoom_out` / `zoom_to`:只调缩放。
|
||||
- `pan_to`:平移到画布坐标或节点附近。
|
||||
|
||||
持久化边界:
|
||||
|
||||
- `persist` 首期固定为 `false`。
|
||||
- 前端执行 view operation 时需要标记 `viewportSource = "agent-transient"`。
|
||||
- autosave 不能因为 Agent transient viewport 变化而触发保存。
|
||||
- 用户随后手动缩放/拖动画布时,可以恢复现有 viewport 保存行为。
|
||||
|
||||
第一期推荐只实现:
|
||||
|
||||
- `focus_node`
|
||||
@ -730,6 +866,27 @@ return {
|
||||
};
|
||||
```
|
||||
|
||||
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 兼容
|
||||
|
||||
修改:
|
||||
@ -819,7 +976,14 @@ export function resolveCanvasViewOperation(params: {
|
||||
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 通道。
|
||||
|
||||
---
|
||||
|
||||
@ -837,6 +1001,7 @@ lib/canvas/agent-context.ts
|
||||
|
||||
- 从旧 `contextPayload` 和新 `canvasAgentContext` 中归一化上下文。
|
||||
- 兼容没有新字段的前端。
|
||||
- 支持没有图片媒体的 slot-only / view-only context。
|
||||
- 限制节点数量,防止请求过大。
|
||||
- 提供 helper:
|
||||
- `getSelectedSlots()`
|
||||
@ -844,6 +1009,13 @@ lib/canvas/agent-context.ts
|
||||
- `getBestAnchorNode()`
|
||||
- `getCanvasAgentFocusPolicy()`
|
||||
|
||||
normalize 优先级:
|
||||
|
||||
1. 如果存在 `contextPayload.canvasAgentContext`,优先使用它。
|
||||
2. 如果没有新 context,再从 legacy `canvasImageNodes` 构造 `candidateNodes`。
|
||||
3. 如果新 context 存在但 `canvasImageNodes` 为空,不允许丢弃 context。
|
||||
4. `selectedSlotCount > 0` 时,媒体输入为空也可以继续规划生成任务。
|
||||
|
||||
### 8.2 新增 mutation 服务
|
||||
|
||||
建议新增:
|
||||
@ -865,13 +1037,38 @@ export async function applyCanvasOperationPlan(params: {
|
||||
|
||||
1. 读取 `canvasProject`。
|
||||
2. 校验 userId 权限。
|
||||
3. 校验 source/slot/anchor 节点存在。
|
||||
4. 计算插入位置。
|
||||
5. 创建或更新节点。
|
||||
6. 创建 edge。
|
||||
7. 写入 metadata。
|
||||
8. 调用 `syncCanvasNodesForProject()`。
|
||||
9. 保存 `canvasProject.canvasData`。
|
||||
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`
|
||||
|
||||
首期 revision 不一致时必须拒绝或要求确认的操作:
|
||||
|
||||
- `replace_node_media`
|
||||
- `delete_node`
|
||||
- `move_nodes`
|
||||
- `replace-existing` slot fill
|
||||
|
||||
### 8.3 新增 API
|
||||
|
||||
@ -893,16 +1090,21 @@ POST /api/canvas/:id/agent-operations
|
||||
{
|
||||
"operationPlan": {
|
||||
"version": 1,
|
||||
"operationId": "op_...",
|
||||
"operation": "append_revision",
|
||||
"projectId": "...",
|
||||
"baseCanvasRevision": "...",
|
||||
"sourceNodeIds": ["..."],
|
||||
"anchorNodeId": "...",
|
||||
"placement": "right",
|
||||
"preserveOriginal": true,
|
||||
"media": {
|
||||
"type": "image",
|
||||
"url": "https://..."
|
||||
}
|
||||
"mediaItems": [
|
||||
{
|
||||
"mediaItemId": "media_...",
|
||||
"type": "image",
|
||||
"url": "https://..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -914,9 +1116,13 @@ POST /api/canvas/:id/agent-operations
|
||||
"success": true,
|
||||
"data": {
|
||||
"createdNodeIds": ["..."],
|
||||
"createdNodeIdsByMediaItemId": {
|
||||
"media-1": "node-1"
|
||||
},
|
||||
"updatedNodeIds": [],
|
||||
"createdEdgeIds": ["..."],
|
||||
"canvasData": {}
|
||||
"canvasData": {},
|
||||
"canvasRevision": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -980,8 +1186,17 @@ 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`
|
||||
@ -1008,14 +1223,42 @@ canvasOperationPlan?: CanvasOperationPlan;
|
||||
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. 调用 `applyCanvasOperationPlan()`。
|
||||
4. 如果 `canvasAgentWritebackMode = "operation_authoritative"`,调用 `applyCanvasOperationPlan()`。
|
||||
5. stream 一个 canvas operation chunk 给前端。
|
||||
6. 如果需要聚焦新结果或当前编辑对象,额外 stream 一个 canvas view 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:
|
||||
|
||||
@ -1028,18 +1271,27 @@ payload:
|
||||
```json
|
||||
{
|
||||
"projectId": "...",
|
||||
"operationId": "op_...",
|
||||
"operation": "append_revision",
|
||||
"createdNodeIds": ["..."],
|
||||
"createdNodeIdsByMediaItemId": {
|
||||
"media_...": "node_..."
|
||||
},
|
||||
"canvasRevision": "...",
|
||||
"anchorNodeId": "...",
|
||||
"handledCanvasMediaItemIds": ["media_..."],
|
||||
"message": "已放到原图右侧"
|
||||
}
|
||||
```
|
||||
|
||||
前端收到后:
|
||||
|
||||
- mutate 当前画布项目。
|
||||
- 或直接 patch 本地 store。
|
||||
- 执行 `CanvasViewOperationPlan`,把视角移动到目标节点或选区。
|
||||
- 暂停 autosave。
|
||||
- mutate 当前画布项目或直接 patch 本地 store。
|
||||
- 记录新的 `canvasRevision`。
|
||||
- 把 `handledCanvasMediaItemIds` 加入本地去重集合。
|
||||
- 恢复 autosave。
|
||||
- 执行 `CanvasViewOperationPlan`,把视角移动到目标节点或选区,且视角变化不触发保存。
|
||||
|
||||
第一期建议 mutate 重新拉取,稳定优先。
|
||||
|
||||
@ -1053,11 +1305,13 @@ payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"viewOperationId": "view_...",
|
||||
"operation": "focus_latest_result",
|
||||
"projectId": "...",
|
||||
"nodeIds": ["..."],
|
||||
"zoom": 0.9,
|
||||
"padding": 120,
|
||||
"persist": false,
|
||||
"reason": "聚焦刚生成的新版本"
|
||||
}
|
||||
```
|
||||
@ -1225,9 +1479,13 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
- 新增 `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 归一化
|
||||
- 超量节点裁剪
|
||||
|
||||
@ -1235,6 +1493,8 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
|
||||
- 现有画布对话不受影响。
|
||||
- Chat API 日志能看到 normalized canvas agent context。
|
||||
- 选中空 slot 且没有任何图片时,Chat API 仍能收到 selected slot。
|
||||
- 用户只发“缩小看全局”时,后端能收到 viewport context。
|
||||
- 未升级前端仍可调用 Chat API。
|
||||
|
||||
### 阶段 2:Agent Slot 前端能力
|
||||
@ -1264,11 +1524,14 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
|
||||
- 新增 `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()`
|
||||
|
||||
验收:
|
||||
@ -1276,6 +1539,8 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
- API 能在指定 anchor 右侧插入图片节点。
|
||||
- API 能填充 slot 并记录 `filledNodeIds`。
|
||||
- API 能创建 revision edge。
|
||||
- 同一个 `operationId` 重试不重复创建节点。
|
||||
- stale autosave 不能覆盖 mutation 新增节点。
|
||||
- 非当前用户无法写入。
|
||||
|
||||
### 阶段 4:Agent 执行链路接入
|
||||
@ -1284,16 +1549,21 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
|
||||
任务:
|
||||
|
||||
- 扩展 direct executor 返回 `canvasOperationPlan`
|
||||
- 扩展 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 边。
|
||||
- 生成后视角自动聚焦新结果。
|
||||
@ -1306,6 +1576,7 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
|
||||
- 新增 `CanvasViewOperationPlan`
|
||||
- 新增 `lib/canvas/view-operation.ts`
|
||||
- 新增 transient viewport 执行通道
|
||||
- 前端消费 `data-canvasViewOperation`
|
||||
- 支持:
|
||||
- `focus_selection`
|
||||
@ -1321,6 +1592,7 @@ lib/ai/runtime/middlewares/canvas-agent-clarification-gate.ts
|
||||
- 用户说“缩小看全局”,画布缩放到主要内容范围。
|
||||
- 生成新图后,画布自动聚焦新结果。
|
||||
- 用户说“对比原图和新图”,画布缩放到两者的共同包围盒。
|
||||
- Agent 自动聚焦不触发 autosave,不改项目保存视角。
|
||||
|
||||
### 阶段 6:标注编辑最小闭环
|
||||
|
||||
@ -1356,10 +1628,16 @@ 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
|
||||
- 多选澄清策略
|
||||
|
||||
@ -1378,6 +1656,10 @@ tests/routes/canvas-agent-operations.test.ts
|
||||
- anchor 不存在 400
|
||||
- fill slot 成功
|
||||
- append revision 成功
|
||||
- 多 `mediaItems` 成功创建多个节点
|
||||
- 返回 `createdNodeIdsByMediaItemId`
|
||||
- 同 `operationId` 重试幂等
|
||||
- stale `baseCanvasRevision` 对高风险操作返回冲突
|
||||
- replace 需要显式 operation
|
||||
|
||||
### 14.3 E2E 手测路径
|
||||
@ -1394,6 +1676,10 @@ tests/routes/canvas-agent-operations.test.ts
|
||||
10. 选中一张图,输入“放大选中的图”,验证画布聚焦该图。
|
||||
11. 输入“缩小看全局”,验证画布缩放到主要内容范围。
|
||||
12. 输入“对比原图和新图”,验证画布同时框住两个节点。
|
||||
13. 选中空 slot 且没有图片,输入“在这里生成一张主图”,验证不被澄清且结果填入 slot。
|
||||
14. 输入“再出三版”,验证只出现 3 个新图,不被旧 `canvasMedia` 重复插入成 6 个。
|
||||
15. Agent 生成期间触发一次普通 autosave,验证新生成节点不被旧快照覆盖。
|
||||
16. Agent 自动聚焦后刷新页面,验证项目默认视角没有被 Agent transient viewport 改掉。
|
||||
|
||||
---
|
||||
|
||||
@ -1403,6 +1689,7 @@ tests/routes/canvas-agent-operations.test.ts
|
||||
|
||||
```ts
|
||||
canvasAgentOperationProtocolEnabled: boolean
|
||||
canvasAgentWritebackMode: "legacy_canvas_media" | "operation_shadow" | "operation_authoritative"
|
||||
canvasAgentSlotEnabled: boolean
|
||||
canvasAgentViewOperationEnabled: boolean
|
||||
canvasAgentAnnotationEditEnabled: boolean
|
||||
@ -1410,12 +1697,14 @@ canvasAgentAnnotationEditEnabled: boolean
|
||||
|
||||
灰度顺序:
|
||||
|
||||
1. 管理员账号开启。
|
||||
2. 内部测试账号开启。
|
||||
1. 开启 context protocol,只读不写。
|
||||
2. 开启 `operation_shadow`,记录 operation 但仍走旧 `canvasMedia`。
|
||||
3. 默认开启 slot,但 mutation 写回只对 Agent 模式开启。
|
||||
4. 开启视角操作能力,只影响当前用户客户端。
|
||||
5. 全量开启非破坏式 `insert_beside` / `append_revision`。
|
||||
6. 最后开放 `replace_node_media`。
|
||||
4. 开启 mutation API,仅内部用户。
|
||||
5. 切到 `operation_authoritative`,前端按 `mediaItemId` 去重旧 `canvasMedia`。
|
||||
6. 开启视角操作能力,只影响当前用户客户端,且不持久化 Agent transient viewport。
|
||||
7. 全量开启非破坏式 `insert_beside` / `append_revision`。
|
||||
8. 最后开放 `replace_node_media`。
|
||||
|
||||
---
|
||||
|
||||
@ -1462,6 +1751,22 @@ canvasAgentAnnotationEditEnabled: boolean
|
||||
- 用户正在拖拽、框选、编辑文本时不强制切换视角。
|
||||
- 提供前端开关 `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 切分
|
||||
@ -1471,6 +1776,8 @@ canvasAgentAnnotationEditEnabled: boolean
|
||||
- 新增协议类型
|
||||
- 扩展 schema
|
||||
- 前端输出 `canvasAgentContext`
|
||||
- 修复 slot-only / view-only context
|
||||
- 增加 `mediaItems`、`operationId`、`mediaItemId`、`baseCanvasRevision`
|
||||
- 后端 normalize
|
||||
- 测试 normalize
|
||||
|
||||
@ -1485,23 +1792,36 @@ canvasAgentAnnotationEditEnabled: boolean
|
||||
|
||||
- 新增 mutation service
|
||||
- 新增 API route
|
||||
- revision 冲突检测
|
||||
- operation 幂等
|
||||
- 批量 `mediaItems`
|
||||
- 支持 `insert_beside` / `fill_slot` / `append_revision`
|
||||
- API 测试
|
||||
|
||||
### PR 4:Agent 生成写回
|
||||
### PR 4:新旧写回迁移
|
||||
|
||||
- 新增 `canvasAgentWritebackMode`
|
||||
- 支持 `operation_shadow`
|
||||
- legacy `data-canvasMedia` 带 `mediaItemId`
|
||||
- 前端 `handledCanvasMediaItemIds` 去重
|
||||
- 验证不开重复节点
|
||||
|
||||
### PR 5:Agent 生成写回
|
||||
|
||||
- direct executor 生成 operation plan
|
||||
- Chat API 调 mutation
|
||||
- 前端刷新画布
|
||||
- `operation_authoritative` 模式写回
|
||||
|
||||
### PR 5:画布视角控制
|
||||
### PR 6:画布视角控制
|
||||
|
||||
- 新增 view operation 类型
|
||||
- 新增 view operation resolver
|
||||
- Chat API stream `data-canvasViewOperation`
|
||||
- 前端执行聚焦、缩放、看全局
|
||||
- transient viewport 不触发 autosave
|
||||
|
||||
### PR 6:标注编辑 MVP
|
||||
### PR 7:标注编辑 MVP
|
||||
|
||||
- annotation metadata
|
||||
- 图片节点批注入口
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user