让 Agent 真正写入 Figma:从 REST API 边界到 Plugin Bridge

快速复现:整段复制给 Codex

不想看完整过程的话,把下面整个代码块复制到 Codex、Claude Code 或其他能操作本机文件与 GUI 的 Agent。它会在当前目录搭建 Bridge,并带你完成真实 Figma 画布验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
请在当前工作目录实现一套安全、可复用的 Figma Agent Canvas Bridge,并完成端到端验证。不要只写教程,要实际创建文件、运行检查,并在 Figma 中生成原生可编辑节点。

1. 先检查运行环境
- 系统以 macOS + Figma Desktop 为目标;未安装时再安装 Figma。
- 确认 Figma 已登录、打开一个具有编辑权限的测试文件。
- 不使用 Figma REST API 修改普通图层;REST 只用于读取或辅助管理,画布写入必须使用 Plugin API。

2. 创建本地开发插件
- 在 ./agent-canvas-plugin/ 下创建 manifest.json、code.js、ui.html、bridge_server.py、bridge_client.py、example.json 和 README.md。
- manifest 使用 documentAccess: "dynamic-page",networkAccess 只允许 http://localhost:3847。
- Plugin 支持 frame、rectangle、ellipse、text、svg、remove。
- 每个节点使用 node.setPluginData("agentCanvasKey", key) 保存稳定 key;相同 key 再次执行时更新原节点,不创建副本。
- 支持 inspect、fonts、apply、move-root-to-page、export-node、audit-root 等白名单 action;禁止执行外部传入的任意 JavaScript。

3. 实现 loopback Bridge
- Bridge 只监听本机 3847 端口,提供 /health、/commands、/next、/results/<id>。
- 使用 IPv4/IPv6 dual-stack socket,让 localhost 与 127.0.0.1 都能访问;manifest 仍保留 http://localhost:3847。
- 限制请求体大小、单次 operations 数量和客户端等待时间,并对 action、key、节点类型做白名单校验。
- 正确执行顺序必须是:启动 bridge_server.py → bridge_client.py 提交命令 → 在 Figma 中运行开发插件。

4. 导入并验证
- 在 Figma 执行 Plugins → Development → Import plugin from manifest…,选择刚创建的 manifest.json。
- 先运行 inspect,证明能读到当前文件、Page 和 selection。
- 运行 example.json,创建一个包含 Frame、标题、正文和 SVG 图标的原生卡片。
- 再运行一次相同命令,验证结果为 update 而不是重复创建。
- 用 audit-root 输出节点类型统计,并确认 imageLayers 为空;不要用整页截图冒充设计图层。
- 用 export-node 调用 Figma exportAsync() 导出 PNG,并检查文字溢出、错位和裁切问题。

5. 交付标准
- 给出目录结构、启动命令、导入步骤、示例 JSON、验证结果和故障排查。
- 重点处理:Plugin 先运行导致空队列、localhost IPv6 导致 Loading、旧 Bridge 进程未重启、字体不存在、SVG 同 key 无法替换内部路径。
- 所有截图和日志必须脱敏:隐藏 Figma file key、node ID、账号、Token、Cookie、本机用户名和绝对路径。
- 最终结果必须是 Figma 原生可编辑图层,并提供 audit-root 与真实 Figma 导出图作为证据。

起因:Agent 能不能直接设计 Figma?

最开始的问题很直接:既然 Figma 提供 API,能不能让 Agent 自动创建页面、写文字、画图标、调整布局,并且留下可继续编辑的图层?

答案是:可以,但不能只靠 REST API。

这个区别非常重要。Figma REST API 很适合读取文件树、节点信息和导出图片,也能管理评论、Variables 与 Dev Resources;但它没有通用的 Frame、Text、Shape 节点增删改接口。真正能够操作当前画布的是运行在 Figma 客户端内部的 Plugin API。

API 适合做什么 能否普通图层 CRUD
REST API 读取文件、跨文件同步、导出、评论、Variables、Dev Resources
Plugin API 在当前打开的文件中创建和修改原生节点
Widget API 持久化、多人可见的画布交互对象 不适合作为批量设计生成器

所以真正的问题变成了:怎样把外部 Agent 的命令,安全、稳定地送进 Figma Plugin?

第一版尝试:固定生成器并不等于 Agent

最早的原型是一个写死的 Figma 插件。点击运行后,它会创建一组固定的 Frame、Text 和 Shape。

这一步证明了两件事:

  1. Plugin API 的确可以写入真实画布。
  2. 生成结果是原生节点,不是贴进 Figma 的截图。

但它仍然不是通用的 Agent 自动化。设计内容被硬编码在插件里,每换一个需求就要改插件代码、重新运行。它更像一个模板生成器,而不是外部智能体可以持续调用的设计执行器。

于是我们把它改造成了一个本地 Bridge。

Bridge 架构

最终采用的是一个很小的 loopback command queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Agent

│ JSON command

Local Bridge ── 127.0.0.1 / localhost only

│ /next

Figma Plugin

│ Plugin API

Native Figma nodes

└── /results/<command-id>

外部 Agent 只提交结构化 JSON,不向 Figma 注入任意 JavaScript。Bridge 只接受白名单 action,例如:

  • inspect:读取当前文件、Page 与选择节点
  • apply:创建或更新图层
  • move-root-to-page:移动到独立 Page
  • audit-root:统计节点类型、图片层与参考覆盖层
  • export-node:调用 Figma exportAsync() 导出真实渲染

Plugin manifest 只允许访问本机地址:

1
2
3
4
5
6
7
{
"documentAccess": "dynamic-page",
"networkAccess": {
"allowedDomains": ["http://localhost:3847"],
"reasoning": "Connects only to the local command bridge."
}
}

这套结构刻意保留了一点“人工在环”:Figma 必须打开,用户必须拥有编辑权限,并且要启动本地开发插件。它不是绕过 Figma 权限体系的无头写入服务。

用稳定 key 保证可重复修改

如果每次生成都创建新节点,画布很快就会堆满副本。解决方式是给每个受管理节点保存稳定 key:

1
node.setPluginData("agentCanvasKey", spec.key)

命令中的节点也带相同 key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"type": "text",
"key": "reader.article.title",
"parent": "reader.screen",
"name": "Article title",
"x": 28,
"y": 130,
"width": 334,
"text": "The Quiet Art of Paying Attention",
"fontFamily": "Georgia",
"fontStyle": "Bold",
"fontSize": 30,
"fill": "#191918"
}

Plugin 先按 key 查找节点:找不到就创建,找到就更新。这样同一份设计描述可以反复执行,也可以只提交两三个节点做局部修正。

这也是“选中一块再重新设计”可行的基础。Plugin API 能读取当前 selection,Agent 可以先 inspect,识别用户选中的 Frame,再把变更限制在这个父节点下面。

最容易踩的几个坑

1. 命令顺序反了

Plugin 的工作模式是一轮只取一条命令,然后立即关闭。正确顺序是:

1
启动 Bridge → 提交命令 → 在 Figma 中运行 Plugin

如果先运行 Plugin,再提交命令,队列是空的,Plugin 会正常退出。这个现象很像“插件什么都没做”,其实只是握手顺序错了。

2. 一直显示 Loading

Bridge 最初只监听 IPv4 loopback,但 Plugin 请求的是 localhost。在某些环境中,localhost 会优先解析到 IPv6,结果是终端访问正常,Figma 内部却一直拿不到命令。

我们先尝试把 manifest 改为 http://127.0.0.1:3847,但 Figma 对 allowedDomains 的校验并不接受这个写法。最终做法是:

  • manifest 继续声明 http://localhost:3847
  • Bridge 使用 dual-stack socket,同时接受 IPv4 和 IPv6 loopback
1
2
3
4
5
6
class DualStackServer(ThreadingHTTPServer):
address_family = socket.AF_INET6

def server_bind(self):
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
super().server_bind()

修复后,localhost127.0.0.1 都能访问同一个队列。

3. “看起来一样”不代表真的完成

一次完整验证至少需要三层证据:

  1. Plugin 返回创建/更新结果。
  2. audit-root 确认节点结构,尤其检查是否存在整页图片层。
  3. export-node 从 Figma 导出实际渲染图,再做视觉检查。

仅看到成功 toast 不够,代码运行成功也不等于文字没有溢出、图标没有错位。

4. 字体会改变布局

我们用 Georgia 做编辑感较强的衬线标题。Reader 页面第一次导出时,标题自动折成三行,压到了副标题。结构审计完全正常,但视觉结果不合格。

最后没有整页重做,只提交了一个两节点 patch:缩小标题字号、调整断行与副标题位置,然后重新导出。这个过程证明局部、幂等更新比每次重建整页可靠得多。

从“贴图”升级到真正的原生设计

为了验证 Bridge 不是把大截图塞进画布,我们做了一个英文美文阅读 App,视觉方向接近 Medium:暖白背景、衬线大标题、宽松留白,只保留少量绿色强调。

页面包括:

  • Today:今日文章和推荐阅读
  • Discover:主题筛选与编辑精选
  • Reader:正文、阅读进度、划句学习工具和音频控制
  • Notebook:生词、音标、释义、来源与复习入口

通过 Plugin API 生成的原生英文阅读 App

最终审计结果:

类型 数量
Frame 52
Text 93
Vector 72
Rectangle 20
Ellipse 3
Image layer 0

一共 240 个原生可编辑节点。状态栏、搜索、书签、导航等图标使用 SVG 创建,文字和布局仍然可以在 Figma 中逐层修改。

当前边界

这个 Bridge 已经适合做页面生成、局部改版和视觉验证,但它不是完整复制 Figma 所有能力的协议。目前刻意保持了较小的白名单:

  • 单次最多 500 个 operations
  • 请求体限制在 2 MB
  • 客户端等待时间有限
  • SVG 内部路径变化时需要 remove 后重建
  • Effects、完整组件变体与 Prototype interaction 还没有纳入协议

这些限制并不完全是缺点。设计自动化最危险的部分,是一个功能过宽的执行器在画布中产生难以审查的副作用。小协议、稳定 key、逐区域执行、每轮导出检查,反而更适合 Agent 工作流。

脱敏与安全处理

本文示例已移除或替换以下信息:

  • Figma 文件 URL、file key、node ID 与账户信息
  • 本机用户名和绝对目录
  • Access Token、Cookie、OAuth 信息
  • 私有项目名、内部评论和真实业务数据

展示图仅包含虚构品牌与示例文章内容。Bridge 只监听本机 loopback,并且不执行远程下发的任意代码。

结论

Agent 当然可以“设计 Figma”,但准确说法应该是:

REST API 负责读取、校验和跨系统集成;Plugin API 负责在已打开、可编辑的 Figma 文件中写入原生画布节点。

真正有价值的不是一次性生成一张看起来像 UI 的图,而是建立一条可重复的工程链路:结构化命令、稳定节点身份、局部更新、原生图层审计,以及最终的真实渲染检查。

当这些环节都存在时,Agent 才从“帮你画一张图”变成了可以参与设计迭代的工程协作者。