Skip to content

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 拖拽创建副本)
  • 复制节点到外部应用(如导出为图片/文本)