Appearance
Feature: 节点复制
一句话目标
用户可以通过右键菜单或快捷键复制节点到剪贴板、粘贴到画布,或直接创建节点副本,快速构建相似节点。
行为约束
约束 1:复制节点到剪贴板
前置条件: 选中节点状态非 generating行为: 右键菜单「复制节点」或 Ctrl/Cmd+C,将选中节点(可多个)序列化为 JSON 写入系统剪贴板;generating 状态的节点被排除 后置条件: 剪贴板包含节点数据
约束 2:粘贴节点
前置条件: 剪贴板中有有效节点 JSON 行为: 右键菜单「粘贴节点」或 Ctrl/Cmd+V,在鼠标当前位置创建节点(多个节点保持相对位置关系),通过 add_node + update_node 两步持久化(先创建获取真实 ID,再写入完整数据) 后置条件: 新节点出现在画布中,数据已持久化,不继承连线,status 重置为 idle
约束 3:复制为副本
前置条件: 节点状态非 generating行为: 右键菜单「复制为副本」,立即在原节点右下方偏移位置创建克隆节点 后置条件: 同约束 2
约束 4:数据继承规则
前置条件: 执行粘贴或副本操作 行为: 继承:类型、标题、prompt、内容(text/images/videos)、生成配置(variations/aspectRatio 等)、model。重置:status → idle。不继承:连线关系、节点 ID 后置条件: 新节点为独立实体
约束 5:generating 状态禁止操作
前置条件: 节点处于 generating行为: 右键菜单「复制节点」「复制为副本」置灰,Ctrl/Cmd+C 跳过该节点 后置条件: 无操作(若全部选中节点均为 generating,则复制不生效)
约束 6:右键上下文菜单
前置条件: 画布已加载 行为: 节点上右键:复制节点、复制为副本、删除节点(移除原有的「复制标题」);空白区域右键:粘贴节点(剪贴板无数据时置灰) 后置条件: 选择操作后执行对应行为
Acceptance Criteria
- [x] AC-01:右键菜单「复制节点」将选中节点序列化为 JSON 写入剪贴板
- [x] AC-02:Ctrl/Cmd+C 将所有选中且非 generating 节点写入剪贴板
- [x] AC-03:右键菜单「粘贴节点」或 Ctrl/Cmd+V 在鼠标位置创建节点
- [x] AC-04:粘贴多个节点时保持原始相对位置关系
- [x] AC-05:「复制为副本」立即在原节点偏移位置创建克隆
- [~] AC-06:新节点继承源节点的 type/title/prompt/model/content/生成配置
- [x] AC-07:新节点 status 重置为 idle,不继承连线
- [x] AC-08:generating 状态节点的「复制节点」「复制为副本」置灰不可点
- [x] AC-09:空白区域右键菜单包含「粘贴节点」,剪贴板无有效数据时置灰
- [~] AC-10:粘贴/副本通过
add_node+update_node两步持久化 - [~] AC-11:节点右键菜单移除「复制标题」
- [~] AC-12:跨画布粘贴正常工作
BDD Scenarios
gherkin
Feature: 节点复制
Scenario: 右键复制单个节点并粘贴
Given 画布中有一个文本节点 "T1",状态为 idle
When 用户右键 "T1" 并点击「复制节点」
And 将鼠标移到画布空白位置
And 按 Ctrl+V
Then 在鼠标位置创建一个新文本节点
And 新节点继承 "T1" 的标题、prompt、model、内容和生成配置
And 新节点 status 为 idle,无连线
Scenario: 快捷键复制多个节点并粘贴
Given 画布中有文本节点 "T1" 和图片节点 "I1",均为 idle
And "T1" 和 "I1" 均被选中
When 用户按 Ctrl+C
And 将鼠标移到新位置并按 Ctrl+V
Then 创建两个新节点,保持 "T1" 与 "I1" 的相对位置关系
Scenario: 复制为副本
Given 画布中有一个图片节点 "I1",状态为 done
When 用户右键 "I1" 并点击「复制为副本」
Then 在 "I1" 右下方偏移位置立即创建一个克隆节点
And 克隆节点继承 "I1" 的全部数据,status 为 idle
Scenario: generating 状态禁止复制
Given 画布中有一个文本节点 "T1",状态为 generating
When 用户右键 "T1"
Then 「复制节点」和「复制为副本」置灰不可点击
Scenario: 多选中包含 generating 节点
Given 选中节点 "T1"(idle) 和 "T2"(generating)
When 用户按 Ctrl+C
Then 仅 "T1" 被复制到剪贴板
Scenario: 剪贴板为空时粘贴置灰
Given 剪贴板中无节点数据
When 用户右键画布空白区域
Then 「粘贴节点」选项置灰
Scenario: 跨画布粘贴
Given 用户在画布 A 中复制了节点 "T1"
When 用户切换到画布 B
And 按 Ctrl+V
Then 在画布 B 的鼠标位置创建节点,数据与 "T1" 一致TDD Pointers
stores/flowStore.ts:
- [~]
addNodeWithData(type, position, initialData)— add_node → resolveNodeId → updateNodeData 链式创建(AC-10) - [~]
duplicateNode(sourceId)— 读取源节点数据,+50/+50 偏移克隆(AC-05, AC-06, AC-07) - [~]
pasteNodes(entries, anchorPosition)— 质心计算 + 并行创建(AC-03, AC-04)
shared/utils/node-clipboard.ts(纯函数):
- [x]
serializeNodes(nodes)→ ClipboardPayload,排除 generating 节点(AC-01, AC-02, AC-08)→test/unit/nodeClipboard.test.ts - [x]
deserializeNodes(json)→ ClipboardPayload,status 重置为 idle,校验 type/position(AC-06, AC-07)→test/unit/nodeClipboard.test.ts - [x] 无效 JSON / 非法 type / 缺失 position 返回 null(AC-09)→
test/unit/nodeClipboard.test.ts
app/utils/clipboardIO.ts(浏览器 API):
- [~]
buildClipboardItem(payload)→ ClipboardItem(text/html + text/plain) - [~]
extractPayloadFromClipboard(items)→ 从 html data-stormflow 属性提取 payload
app/components/nodes/NodeShell.vue(右键菜单):
- [~] 菜单项中无「复制标题」(AC-11)
- [~] generating 状态下复制/副本项 disabled(AC-08)
app/composables/useCanvasEditor.ts(画布右键菜单):
- [~] 空白区域右键包含「粘贴节点」,剪贴板无数据时 disabled(AC-09)
Out of Scope
- 剪贴板历史/多次粘贴管理
- 复制时保留连线关系
- 撤销复制/粘贴操作(依赖通用 undo 系统)
- 拖拽复制(按住 Alt 拖拽创建副本)
- 复制节点到外部应用(如导出为图片/文本)