Agent Skillshenjicc/Henji-AI › canvas-node-builder

canvas-node-builder

GitHub

指导在 Henji-AI 画布中新增或改造 ReactFlow 节点。涵盖判断实现路径、在 nodeRegistry 声明定义,以及组装 ModelInputRow 等标准化参数行组件,确保 UI 与交互规范一致。

.claude/skills/canvas-node-builder/SKILL.md henjicc/Henji-AI

Trigger Scenarios

新建一个画布节点 加一个 XX 节点 节点 UI 不规范/不一致,按标准改一下 节点的图片/视频/音频输入要怎么接

Install

npx skills add henjicc/Henji-AI --skill canvas-node-builder -g -y
More Options

Non-standard path

npx skills add https://github.com/henjicc/Henji-AI/tree/main/.claude/skills/canvas-node-builder -g -y

Use without installing

npx skills use henjicc/Henji-AI@canvas-node-builder

指定 Agent (Claude Code)

npx skills add henjicc/Henji-AI --skill canvas-node-builder -a claude-code -g -y

安装 repo 全部 skill

npx skills add henjicc/Henji-AI --all -g -y

预览 repo 内 skill

npx skills add henjicc/Henji-AI --list

SKILL.md

Frontmatter
{
    "name": "canvas-node-builder",
    "description": "Henji-AI 画布(ReactFlow)新增\/改造节点时使用。指导如何选择节点实现方式(GenerationNodeShell 复用 \/ 标准行组件拼装 \/ 纯展示节点)、如何在 nodeRegistry.ts 中声明 CanvasNodeDefinition、如何组装 ModelInputRow\/MediaInputRow\/NodeParamRows、特殊节点如何在标准行组件之上叠加专属面板。普通模型 schema 会被标准生成节点自动读取;只改供应商、模型参数、显隐、计价或请求构建时不使用本 skill。触发场景:用户要求\"新建一个画布节点\"、\"加一个 XX 节点\"、\"这个节点 UI 不规范\/不一致,按标准改一下\"、\"这个节点的图片\/视频\/音频输入要怎么接\"。"
}

Canvas Node Builder

这套体系是什么

Henji-AI 画布节点不是各写各的 UI,而是从一组标准化"参数行组件"拼装出来,结构和分层细节见 docs/rules/canvas.md。本 skill 提供落地这两条规则的具体步骤、字段取值依据和真实代码片段。

核心组件(src/features/canvas/params/):

组件 职责
ModelInputRow 模型选择行(标签 + MODEL 端口 + 模型 chip)
MediaInputRow 媒体输入行(图片/视频/音频;本地上传 + 缩略图 + 拖拽排序 + 上游连线只读态)
NodeParamRows 标量参数逐行渲染,按 schema.order 排序,每参数一行
NodeInputRows 上面三者的编排容器:模型行 → 媒体行 → 参数行

壳层(src/features/canvas/nodes/shared/GenerationNodeShell.tsx):标题/价格/提示词框/NodeInputRows/端口/resize 全部内置,新增一个"标准生成节点"时大概率只需要传 props,不需要写 UI。

先判断是否真的需要改画布

  • 标准生成节点通过 GenerationNodeShell -> NodeInputRows -> NodeParamRows -> NodeParamControl 读取模型注册源。新增/修改模型参数、显隐、联动、计价和 builder 后,画布通常会自动更新,不需要节点文件同步一份参数。
  • 不要因为“这个模型能在画布里选择”就新增节点组件、修改 nodeRegistry.ts,或在节点里写模型 ID 分支。
  • 只有以下情况才继续使用本 skill:新增节点类型;改变节点端口/媒体行/结果节点;增加节点内容区的独有交互;或共享 NodeParamControl 无法表达已经确认的新参数类型。
  • 新增复合/特殊参数面板但仍属于模型 schema 时,先保证 ParamRendererNodeParamControl 共用同一个注册面板和值结构;这属于共享参数呈现,不是节点专属面板。只有确实需要节点 DOM/画布交互时才走路径 B。
  • 模型 schema 中的特殊参考图、遮罩、PDF 等上传参数必须由共享 NodeParamControl 呈现上传入口,不能在画布退化成 URL 文本框,也不能为单个模型复制上传 UI;供应商上传仍由生成运行时统一完成。

第一步:判断节点该怎么实现

节点有"生成"动作(调模型出图/视频/音频),且没有独有交互?
  → 直接复用 GenerationNodeShell(见下方"路径 A"),新文件约 20~30 行

节点有生成动作,但有独有交互(比如分镜的格子编辑器、特殊预览区)?
  → 自己写节点组件,但内部拼装 ModelInputRow/MediaInputRow/NodeParamRows(见"路径 B")

节点没有参数/生成行为,纯展示或纯数值源(如 ImageNode 展示节点、IntSourceNode)?
  → 不套用本 skill 的行组件体系,照搬同类节点已有写法即可

判断"是否需要 'rows' 端口形态":节点只要声明 ports.target.acceptsimage/video/audio 中任意一种,就必须在 connectivity 里加 targetHandleMode: 'rows',并用 MediaInputRow 渲染对应媒体行——禁止新增节点手写单一 id="target" 的 Handle 来接收媒体(旧节点遗留的 legacy 写法,不要再复制)。

判断“参数组如何呈现”:panel / composite 只能在节点中占一行摘要触发器,详细内容用 ParamGroupTrigger 打开节点布局流之外的浮动特殊面板;禁止在节点内部直接展开整组参数并撑高节点。组内只有已经连线、需要持续显示连接状态的参数保留为紧凑行。打开和关闭面板前后,ReactFlow 测量高度必须不变。

端口必须复用 NODE_PORT_*_CLASS 与已登记的媒体/数据类型语义 token,不要在节点调用点任意挑尺寸或颜色。视觉核心保持轻量(当前 8 CSS px),用透明扩展区维持至少 24 CSS px 的可点范围;未连接端口空闲时隐藏,只在对应行/节点悬浮或正在连线时短暂显现,已连接端口保持可见。端口或 token 改动后,除静态检查外必须查看真实 Electron 画布截图。

路径 A:完全复用 GenerationNodeShell

适用于"标准生成节点":一个提示词框 + 模型/媒体/参数行 + 生成出一个结果节点。AI 图片/视频/音频节点都是这样实现的。

  1. canvasNodes.ts:加节点类型常量到 CANVAS_NODE_TYPES,加 XxxNodeData 接口(继承/对齐 GenerationNodeShellData),需要的话加类型守卫。
  2. nodeRegistry.ts:加一个 CanvasNodeDefinition,参考 imageEditNodeDefinition(约第 159 行起)。关键字段取值见 references/node-registry-fields.md,别凭空猜字段含义。
  3. nodes/XxxNode.tsx:整份组件只是 GenerationNodeShell 套了一层 props,参考:
// src/features/canvas/nodes/ImageEditNode.tsx(完整文件,约 20 行)
export const ImageEditNode = memo(({ id, data, selected, width, height }: ImageEditNodeProps) => (
  <GenerationNodeShell
    id={id}
    nodeType={CANVAS_NODE_TYPES.imageEdit}
    data={data as GenerationNodeShellData}
    selected={selected}
    width={width}
    height={height}
    icon={<Sparkles className="h-4 w-4" />}
    promptPlaceholderKey="node.imageEdit.promptPlaceholder"
    promptRequiredKey="node.imageEdit.promptRequired"
    apiKeyRequiredKey="node.imageEdit.apiKeyRequired"
    resultTitleKey="node.imageEdit.resultTitle"
    resultNodeExtraData={{ resultKind: 'generic' }}
  />
));
  1. nodes/index.ts:把新组件加进 nodeTypes 映射(key 是 CANVAS_NODE_TYPES 里的值)。
  2. i18n:补 node.menu.xxxnode.xxx.promptPlaceholder/promptRequired/apiKeyRequired/resultTitle 等 key(zh-CN / en-US 都要)。
  3. docs/rules/testing.md 选择最小验证:运行节点/注册表精确测试;只有改到模型 manifest 或翻译时才运行 gen:model-manifest / check:model-i18n,不要默认跑全量 lint。

价格徽标、生成按钮、提示词框、端口、resize 全部由 GenerationNodeShell 内置,不要在这层重新实现任何一项。

路径 B:自定义节点 + 拼装标准行组件

适用于"有独有交互的生成节点",比如分镜生成节点(格子编辑器)。完整真实案例和踩坑点见 references/special-node-pattern.md,这里只给装配公式。

目录约定:专属 UI(格子编辑器、特殊预览面板等)放 nodes/<节点名>/ 子目录,节点主文件 nodes/XxxNode.tsx 只做编排和接线,不在专属面板里重新实现模型选择/媒体上传/参数渲染。

节点主文件的标准骨架

<div /* 节点外壳 */>
  <NodeHeader
    titleText={resolvedTitle}
    editable
    onTitleChange={...}
    rightSlot={effectiveModel && (
      <PriceEstimate
        providerId={effectiveModel.meta.provider}
        modelId={effectiveModelId}
        params={modelParamValues}
        variant="badge"
      />
    )}
  />

  {/* 专属面板:本节点独有的交互区域,比如格子编辑器 */}
  <XxxSpecialPanel ... />

  {/* 标准行区:模型行 → 媒体行(按需)→ 参数行,三者都是现成组件,不要重写 */}
  <div className="flex shrink-0 flex-col gap-1.5">{/* NODE_ROW_GAP_CLASS */}
    <ModelInputRow
      mediaType="image"
      modelId={selectedModelId}
      overrideModelId={overrideModelId}
      storedParams={nodeData.params}
      onModelChange={handleModelChange}
      onParamsChange={handleParamsChange}
      incomingImages={effectiveImages}
    />
    {imageRowMax > 0 && (
      <MediaInputRow
        nodeId={id}
        mediaKind="image"
        label={t('node.mediaRow.image')}
        maxCount={imageRowMax}
        inlineValue={mediaInputs.image ?? []}
        onInlineChange={handleImageInputChange}
      />
    )}
    <NodeParamRows
      nodeId={id}
      schema={modelParamSchema}
      values={modelParamValues}
      setParam={setParam}
      excludeParamIds={['prompt', 'text']}
    />
  </div>

  <Handle type="source" id="source" position={Position.Right} ... />
  <NodeResizeHandle ... />
</div>

必须配套的状态/逻辑(缺一个就会出现"看起来标准但行为不对"的 bug):

  • 模型覆盖:用 getConnectedParamIds/collectInputValuesgraphValueResolver.ts)算出 overrideModelIdeffectiveModelId = overrideModelId ?? selectedModelId,所有 schema/生成逻辑用 effectiveModelId,节点自身存储字段仍用 selectedModelId。完整写法照抄 GenerationNodeShell.tsx 第 207-254 行或 StoryboardGenNode.tsx
  • 本地上传双态:节点 data 加 mediaInputs?: Partial<Record<RowMediaKind, string[]>> 字段;effectiveImages = incomingImages.length > 0 ? incomingImages : (mediaInputs.image ?? []);所有"用图片做什么"的逻辑(生成参数、智能宽高比检测、@引用列表)都要用 effectiveImages,不要漏改成只用 incomingImages
  • useNodeModelParams 要传 media:调用处补上 media: { images: effectiveImages, videos: effectiveVideos, audios: effectiveAudios }(按节点实际支持的媒体类型取舍),否则 modelParamValues 里不会有 images/videos/audios,模型 schema 里依赖"是否已上传图片/视频"的 visible.condition/pricing.calculator/linkage 在画布里会静默失效(不报错,只是永远判断成"没有媒体")。只有节点自己持有真实媒体状态时才传;如果是另一个共享同一份 storedParams 的次要 useNodeModelParams 实例(如参数摘要 chip),不要传。详见 references/special-node-pattern.md 第 2 点。
  • 数量上限resolveInputLimits(effectiveModelId, modelParamValues).images.max 决定 MediaInputRowmaxCount,同时决定要不要渲染这一行(max > 0 才渲染)。
  • 生成按钮nodeRegistry.ts 里该节点的 capabilities.toolbarGenerate: true,节点内部 useEffect(() => canvasEventBus.subscribe('generation/run', ({nodeId}) => { if (nodeId === id) void handleGenerate() }), [...])不要在节点内容区画一个"生成"按钮——AI 图片/视频节点都没有,生成由选中节点后浮出的顶部工具条触发。
  • 连接端口nodeRegistry.tsconnectivity.targetHandleMode: 'rows';如果这是从 legacy 单一 target Handle 迁移过来的旧节点类型,必须同时检查 nodeMigrations.tsmigrateLegacyTargetHandle 是否已覆盖该类型(它按 ports.target.accepts 只有一种媒体类型时自动迁移旧边,多媒体类型节点需要单独处理,不要假设自动生效)。

检查清单(改完自查)

  • 价格徽标在 NodeHeaderrightSlot,不在内容区
  • 没有手写的模型选择 chip / 媒体缩略图 / 逐行参数布局(这些是 ModelInputRow/MediaInputRow/NodeParamRows 的职责)
  • 没有手写单一 id="target" 的 Handle 来接收媒体(targetHandleMode: 'rows' + MediaInputRow 替代)
  • 没有节点内置"生成"按钮(capabilities.toolbarGenerate + canvasEventBus 替代)
  • panel / composite 使用单行触发器和节点外浮动面板,开关面板不改变节点高度
  • 接入点复用共享尺寸与语义 token;空闲未连接时隐藏,交互时显现,已连接时保持可见,缩放后仍不过分抢眼
  • 节点自身的 useNodeModelParams 调用传了 media(除非它是共享 storedParams 的次要实例)
  • nodeRegistry.tsCanvasNodeDefinition 字段填全,对照 references/node-registry-fields.md
  • 已按 docs/rules/testing.md 跑节点/注册表精确测试及本次真正涉及的颜色、模型 i18n、类型专项检查;没有无理由叠加全量命令

Version History

  • 19fcc12 Current 2026-08-28 23:13

    完善模型参数体验与画布路由;修复连接点显示逻辑;优化媒体链接参数为供应商上传。

  • d5d1dd1 2026-08-20 11:47

Same Skill Collection

.claude/skills/henji-ai-adaptation-assistant/SKILL.md
.claude/skills/henji-application-capability/SKILL.md
.claude/skills/henji-model-adaptation/SKILL.md
.claude/skills/henji-ui-surface/SKILL.md
.codex/skills/canvas-node-builder/SKILL.md
.codex/skills/henji-ai-adaptation-assistant/SKILL.md
.codex/skills/henji-application-capability/SKILL.md
.codex/skills/henji-model-adaptation/SKILL.md
.codex/skills/henji-ui-surface/SKILL.md
resources/assistant-skills/三维镜头构图/SKILL.md
resources/assistant-skills/图片生成/SKILL.md
resources/assistant-skills/生成排障/SKILL.md

Metadata

Files
0
Version
19fcc12
Hash
1b94ca55
Indexed
2026-08-20 11:47

trang chủ - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-04 10:27
浙ICP备14020137号-1 $bản đồ khách truy cập$