项目开源地址:https://github.com/GuoBug/PatchCat
在线体验:https://guobug.github.io/PatchCat/
往期回顾:
📖 《从 0 到 1 打造 AI 提示流编排器:即时对话调试抽屉、节点级耗时追踪与一键发布生产 API 实战(开源系列 07)》
📖 《从 0 到 1 打造 AI 提示流编排器:给大模型装上手和脚!Agent 节点与 Tools 工具调用体系设计与实战(开源系列 09)》
📖 《从 0 到 1 打造 AI 提示流编排器:酒香也怕巷子深,如何让用户真正接受产品?空白画布引导与场景模板实践(开源系列 10)》
一、 鞋里有沙走不远:为什么手感细节决定成败?
在上一篇文章里,我们探讨了怎样通过自适应卡片和场景模板,让新朋友能够进得来、看得懂,把产品的门面和起步门槛打磨平实。
但当大家真正满怀兴致坐下来,准备搭一个包含 5~10 个节点的实际业务流程时,真正的考验才刚刚开始。
常言道,“鞋里有沙走不远”。很多技术工具在功能列表上看似样样齐全,但一旦真正坐下来连续用上一两个小时,你往往会遭遇几种让人抓狂的细碎摩擦:
- 连线断裂的挫败感:从端口拉出一条连线往空白处一扔,结果“咻”的一声连线缩回去了,必须退回侧边栏重新拖节点、放好位置、再重新拉线连一次;
- 节点堆叠乱成一团:如果允许随便加节点,新加的卡片常常直接“砸”在老卡片脸上,连线交叉遮挡,越排越乱;
- 误操作的心惊胆战:不小心手滑误删了一个写了上千字 Prompt 的关键节点,按一下
Ctrl+Z发现毫无反应,半天的心血瞬间蒸发。
这些琐碎的阻滞,就像掉进鞋子里的一粒粒小沙子。虽然不至于让程序当场崩溃,但走不了几步就会把人的耐心消磨殆尽,最后烦躁地把网页关掉。
真正好用的生产力工具,绝不仅仅是把功能给做齐,更重要的是保护好使用者的思考流不被各种笨拙的机械操作打断。
二、 连线断裂之痛:为什么要做“松手就地加节点”?
1. 传统连线方式的机械重复
回忆一下传统的工作流连线体验:
- 想要给当前节点后面接一个大模型或者代码清洗步骤,你必须先把鼠标挪到屏幕最左侧的节点菜单;
- 找到对应的卡片,按住鼠标拖进画布;
- 小心翼翼在空白处找准位置放下;
- 再把视线拉回到刚才的上游节点,拉住输出端口,对准新节点的输入端口连上去。
搭一个三四个节点的小 Demo 还能忍受,但一旦面对复杂的分支、质检、聚合流,这一套“拖拽 ➔ 放下 ➔ 回头连线”的动作要重复几十遍。更让人沮丧的是,有时候顺手拉出一根线想要接新节点,拉到空白处一松手,连线直接灰溜溜地缩了回去,刚才脑子里涌出来的逻辑思路瞬间断档。
2. 人机共创:把思维导图的丝滑感搬到画布上
在打磨 v0.4.6 的连线体验时,我和 AI 搭档推演:为什么不能像大家平时用思维导图(比如 XMind、飞书脑图)那样,拉出一根线往空地上一扔,就地弹个小菜单,点一下直接把新节点建好、线也顺手连上?

-
我提出的体验要求: 连线在空白处释放的瞬间(
onConnectEnd),微调选单必须在鼠标松手的正下方弹出来;点选任意节点后,系统必须同时完成“生成新节点”与“自动连线”两件事,鼠标根本不需要离开当前工作区。 -
AI 搭档提醒的关键工程细节: 画布是支持缩放(Zoom)和平移(Pan)的。浏览器原生事件拿到的鼠标位置是屏幕物理像素(
clientX, clientY),如果不通过 React Flow 的视口变换矩阵(screenToFlowPosition)做精准逆变换,一旦用户放大或缩小过画布,弹出的菜单就会严重“漂移”,离鼠标落点十万八千里。
通过把屏幕坐标精确换算为画布内部世界坐标,我们实现了“指哪打哪”的松手建连体验(DropToAddMenu.tsx)。
三、 卡片别糊在脸上:用长方形防撞规则(AABB)自动找空位
1. 随之而来的新麻烦:节点重叠
松手就地建连确实大幅减少了鼠标往返移动,但很快我们就在实机测试中遇到了一个尴尬的新问题:
如果新节点只是傻傻地生成在鼠标松手的位置,而那个位置旁边正好已经排布了其他节点,新生成的长方形卡片就会“直接压在老卡片身上”。两个卡片叠死在一起,文字遮挡、连线交叉,用户不仅看不清内容,还得停下手头的思路,手动把新卡片挪开。
如果每次建完节点还要手动去“搬砖”,那这种快捷建连就成了负优化。
2. 从游戏开发里借工具:长方形防撞规则(AABB)
为了解决卡片互相遮挡的问题,AI 搭档提出了一个成熟的数学模型——AABB(Axis-Aligned Bounding Box,轴对齐包围盒)碰撞检测。
名字听着很吓人,但扒开外壳,其实就是最简单的小学几何:
- 在画布上,我们所有的节点卡片都是方方正正的长方形(标准宽 280 像素、高 180 像素);
- 所谓“轴对齐”,就是长方形的四条边永远平行于屏幕的水平 X 轴和垂直 Y 轴,规规矩矩立着,不会歪斜旋转;
- 算一算两个卡片有没有重合,只需同时看两件事:
- 水平方向:新卡片的左右两边,有没有插进老卡片的左右范围里?
- 垂直方向:新卡片的上下两边,有没有插进老卡片的上下范围里?
如果横向和纵向都重叠了,那就说明两个卡片“撞车”了!
// 朴素而高效的矩形重叠检测核心
const overlapX = x < nx + nw + gap && x + nodeWidth + gap > nx;
const overlapY = y < ny + nh + gap && y + nodeHeight + gap > ny;
3. 两条朴素的工程避让规则
在检测到碰撞后,新卡片应该往哪躲?结合大模型工作流的实际排布习惯,我们定了两条非常朴素的避让规约(resolveAABBCollision):
- 顺着水流往右挪(留足呼吸间距): 有向图的数据流向天然是从左往右推进的。一旦检测到新卡片压到了已有节点,算法自动将新卡片顺着右边移开,并且强制留出至少 40 像素的呼吸间距;如果移到右边后发现撞到了第三个节点(连环追尾),就顺次继续向右寻找空位。
- 太宽了就换一行(防止无休止横向拉伸): 如果一条链条太长,一直向右排,横向坐标一旦超过了 1800 像素(普通显示器差不多快装不下了),算法就会自动让新节点折到下一行、从左侧重新起排。

为了确保避让逻辑不会在节点密集时陷入死循环,我专门编写了极端单元测试(tests/canvas-ergonomics.node.test.ts):模拟在只有几个像素的狭窄空间里连续塞入 10 个节点,验证最大 20 次安全跳出的防死锁机制,确保新节点无论何时都能稳妥就位。
四、 误操作的救命稻草:把加节点和连线绑在一起撤销
手感好不仅体现在“加得快”,更体现在“改错时心里有底”。
在画布操作中,最让人崩溃的莫过于手滑删掉了节点或者连错了线,却找不到后悔药。
1. 为什么普通的撤销容易踩坑?
在实现“松手建连”之后,撤销(Undo)机制遭遇了一个棘手的隐患: “松手就地加节点”在底层实际上同时触发了两个动作——在画布上新建了一个节点实体,以及在上游和新节点之间建立了一条边(Edge)。
很多工具之所以撤销做得稀烂,就是因为这两个动作被分成了两次独立记录。用户按下 Ctrl+Z,界面上只是节点消失了,但那条连线却被孤零零地留在了画布上。这条悬空的“断头线”不仅极度难看,而且会让底层的拓扑排序调度器在运行时直接抛出 Cannot read property of undefined 的致命报错。
2. 绑在一起,撤销就得干干净净
为了让撤销利落,我们在 HistoryManager 中将“新增节点”与“建立连线”捆绑为一次完整动作进行快照归档:

这样一来,当用户按一下 Ctrl+Z 时,新生成的节点和刚刚连上的那根线会同时干干净净地消失,绝对不留任何残缺数据。
顺着这个底座,我们把键盘常用的高频快捷键也一并补齐了:
Ctrl+Z / Ctrl+Y:撤销与重做;Ctrl+C / Ctrl+V:多选节点批量复制粘贴。粘贴时同样自动附带一个 30 像素的微小位移,防止复制出的新节点严丝合缝地盖在原节点上方;Delete / Backspace:一键清除选中元素。
五、 总结与体会:好工具的手感是抠出来的
回过头来复盘这一阶段的交互打磨,心里有一点很真实的体会:
在很多人的潜意识里,大模型系统最酷炫的一定是底座模型的能力、或是 Agent 自主思考时的 ReAct 循环。但如果回到一个真实使用者的日常状态下,决定一个人愿不愿意每天打开这个工具去解决问题的,往往正是这些最不起眼的手感细节。
花时间去算 40 像素的避让间距、去较真坐标系缩放漂移、把撤销做得利落干净……这些工作听起来远没有“又接入了一个超强开源模型”那么抓人眼球。但正如那句老话所言,“鞋里有沙走不远”。只有耐着性子把这些卡手卡脚的沙子一粒粒倒干净,一个工具才能真正让人用得舒服、走得长远。
当然,自己也是边做边学,目前在手势触控、复杂网格的自动拓扑重排(Auto-Layout)方面肯定还有不少粗糙的地方。非常欢迎社区的朋友们上手试试手感,有任何别扭的地方随时提 Issue 拍砖指教!
项目开源地址:https://github.com/GuoBug/PatchCat
在线体验地址:https://guobug.github.io/PatchCat/
如果文章对你有所启发,欢迎在 GitHub 上点个 Star 🌟,更欢迎资深工程师与产品同行提出你的宝贵指教!