为AI编码Agent提供设计对照能力:将设计稿覆盖在页面截图上,返回匹配分数和差异区域,指导修复直到设计稿与实现完全一致。
一个编码 Agent 能在一秒内写完标签、接好样式、交付一个页面。它甚至可以"看"到那个页面。但它做不到的是逐像素地将页面与设计稿对照审视。瞟一眼截图,它不会发现标题低了几个像素、颜色冷了一度、字体压根没加载成功。它靠肉眼构建,页面与设计稿渐行渐远,却没有任何精确的手段来捕捉这种偏差。
Design Diff 给了它这个度量工具。它将设计稿覆盖在实时页面的截图上——两者尺寸完全一致——然后返回一个数字表示匹配程度,以及一个圈出差异区域的边界框。调用它,读取分数,修复边界框指出的问题,再次调用。这个循环一直运行直到页面与设计稿一致,而全程无需任何人盯着。
它从一开始就是为 Agent 设计的,提供命令行和库两种接口,均以纯 JSON 交互,因此任何 Agent 或脚本都可以驱动它。同样的信号对人类同样友好——无论是开发者做交接验收、设计工程师构建系统,还是设计师想要的是证明而非承诺。
它运行在 Bun 上,无需全局安装,因为 bunx 可以一步获取并运行它。第一次运行会下载 Chromium,之后每次都即时执行。
你有一个运行中的页面和一张设计稿 PNG 图片。将工具指向两者,让它们并排对比。
bunx design-diff http://localhost:3000 --png home.png --open
它以三种方式给出答案。
终端中的简短报告。
浏览器中的交互页面,让你将一张图片叠加在另一张之上。
以及一个产物文件夹,默认写入 .design-diff,每次运行带时间戳,或者通过 --out 指定输出位置。
Agent 跳过浏览器直接读取 JSON,但循环是一样的——问与答,往复直到匹配。
DESIGN DIFF
────────────────────────
Visual match 92.4%
Diff bounds
x=0..1440
y=812..980
page coverage=17.1%
百分之九十二的像素对上了。剩下的都聚集在画面底部,就在我们标注的那几行之间。还没打开报告你就知道该看哪里了——知道问题出在哪里,问题就已经解决了一半。
本文中出现的每个参数都有上下文说明,design-diff --help 列出了它们的完整列表。
这个循环只有在数字可信的情况下才能运作,有三件事保证了这一点:一个共同的参照系、在正确时刻捕捉的页面、以及一目了然的分数。
测量需要共同的基础。两样东西只有在同一尺度下才能比较,所以设计稿设定尺寸,页面按照那个精确的宽高来截图。放在同一刻度上,它们逐点对齐,没有假读的空间。
Design Diff 不会匆忙截图。它等待字体加载完成、图片就位、网络稳定,并且关闭动画和闪烁的光标。大多数假差异都来自在页面渲染中途截了图。如果字体还没准备好,它会在输出中说明,而不是悄无声息地给你一个糟糕的分数。对于一个在循环中追求匹配的 Agent 来说,这就是保持信号诚实的关键。数字只有当页面真正发生变化时才会变动,而不是因为被抓取得太早。
每次运行都会打印三个度量值,并将它们写入运行文件夹中的 metrics.json。
差异的形状就是诊断结果。按钮上的一个小框是颜色填错了。贯穿整个页面的大框是字体没加载或者视口不匹配。看形状,不要只看数字。数字说有问题,形状说是什么问题。对于 Agent 来说,这是一个现成的奖励信号——一个可以攀登的匹配百分比,以及指向下一次编辑的差异边界。
{
"url": "http://localhost:3000",
"matchPercent": 92.4,
"diffPercent": 7.6,
"changedPixels": 107251,
"totalPixels": 1411200,
"coveragePercent": 17.1,
"diffBounds": { "x": 0, "y": 812, "width": 1440, "height": 168 },
"readiness": { "fontsReady": true, "imagesComplete": true },
"paths": {
"design": ".design-diff/2026-08-16T14-22-09-482Z/design.png",
"page": ".design-diff/2026-08-16T14-22-09-482Z/dom.png",
"heatmap": ".design-diff/2026-08-16T14-22-09-482Z/heatmap.png",
"overlay": ".design-diff/2026-08-16T14-22-09-482Z/overlay.html",
"metrics": ".design-diff/2026-08-16T14-22-09-482Z/metrics.json"
}
}
真实工作比"干净的设计稿对着静态页面"要混乱得多。Design Diff 在现实所在的地方与它相遇——无论真相来源在 Figma、页面带有注定会变化的内容、加载较慢、需要登录才能访问,还是只存在于磁盘上的截图。
给它一个文件 key 和一个 frame id,它直接从 Figma API 获取帧的尺寸和 PNG。
export DESIGN_DIFF_FIGMA_TOKEN=figd_your_token
bunx design-diff http://localhost:3000 --file abc123 --frame 10-2 --open
frame id 就是帧 URL 中的那个,比如 10-2。只读 token 就够了。这是最接近实时交接的方式。设计师动了一个组件,你再跑一次,立刻看到偏差。Agent 可以直接瞄准 Figma 帧作为目标,完全不用碰本地文件。
真实页面带有注定会变化的东西。头像、时间戳、实时计数器、随机英雄图。把这些算作错误,是在和页面的本质较劲。将它们遮罩掉,这样它们永远不会被计入差异。
按固定矩形遮罩,单位是 CSS 像素,可以重复使用任意次。
bunx design-diff http://localhost:3000 --png home.png \
--ignore 24,24,48,48 --ignore 0,900,1440,120
或者按 CSS 选择器遮罩,当区域在多次运行之间移动或调整大小时,这样更明智。每个匹配的元素都用它自己的实时边界框来遮罩。
bunx design-diff http://localhost:3000 --png home.png \
--ignore-selector "[data-dynamic], time, .avatar"
按选择器遮罩是将不稳定分数变成稳定分数的关键。它消除了从来不是真正差异的噪音。对于在循环中追求 100% 的 Agent 来说,稳定的分数就是一切,因为它需要数字只反映它自己的编辑,不受其他任何东西影响。
bunx design-diff http://localhost:3000 --png home.png --wait-for ".hero-loaded"
用 Playwright 保存一次会话,然后交给它。凭据永远不会接触到工具本身。保存的文件只包含 cookie 和本地存储。过期了就重新做一个。
bunx playwright codegen --save-storage=auth.json https://your-app/login
# log in in the window, then close it
bunx design-diff https://your-app/dashboard --png dash.png --auth auth.json --open
bunx design-diff --actual screenshot.png --png design.png --json
这是 Agent 在紧凑循环中使用的快速路径。它已经通过自己的方式捕获了页面,所以 Design Diff 只对比两个文件,完全不启动浏览器。它可以离线运行、在测试中运行、在沙盒中运行。没有实时页面就没有等待,也没有选择器遮罩,scale 也会被忽略因为没有屏幕需要响应,所以边界以普通图像像素返回。其他一切都一样。
同一次运行服务于两个截然不同的读者。机器想要一个数字和一个框。人想要看到它。Design Diff 一次执行同时给予两者各自需要的东西。
HTML 报告不是一张静态图片。它是一个将两张图片合二为一的工具。
Reveal 滑块在设计稿和页面之间 wipe 切换。拖动手柄将接缝移过画面。
透明度滑块将设计稿淡入页面,这样小的偏移就会显现出来。
热力图点亮每个变化了的像素,让一切无所遁形。
边界框勾勒出变化区域,让你的视线直接聚焦到那里。
它对视网膜屏幕安全。当你的导出图以两倍大小绘制时,传入 --scale 2 它会保持度量的准确性。
HTML 报告在你面前很好,但它没法进入 pull request 评论。传入 --annotate 它会将差异框直接画在页面截图上。
bunx design-diff http://localhost:3000 --png home.png --annotate
查看它不需要浏览器。把它丢进 review、Slack 线程、CI artifact。没有差异时不会写入任何东西,所以带注释的文件始终意味着有东西值得看。
--json 只将 metrics 对象打印到 stdout,这样脚本或 Agent 读取时无需解析。--fail-under 设定了门槛。如果视觉匹配度降到以下,进程以 1 退出,构建变红。bunx design-diff http://localhost:3000 --png home.png --json --fail-under 98
这就是 Agent 合约。读取 JSON,根据 matchPercent 和 diffBounds 行动,分数够高时停止。
有一点要分清。--fail-under 是一个相对于视觉匹配的百分比。它不是 --threshold,后者是逐像素的颜色敏感度。一个守门,一个调节每个像素比较的严格程度。当你要衡量很多页面,或者反复衡量同一个页面时,传入 --no-overlay 跳过 HTML 只保留 metrics。
CLI 只是一个便利。真正的循环存在于库中,是一个你可以飞快调用的函数。
CLI 是对一个函数的薄封装,它返回它计算出的所有东西,以及每个产物的路径。
import { designDiff } from "design-diff"
const result = await designDiff({
url: "http://localhost:3000", // or actual: "screenshot.png" for image mode
design: "home.png", // or { fileKey, frameId }
scale: 1,
threshold: 0.1,
ignore: [{ selector: "[data-dynamic]" }, { x: 24, y: 24, width: 48, height: 48 }],
waitFor: ".hero-loaded",
})
if (result.matchPercent < 98) process.exit(1)
你得到 matchPercent、diffPercent、changedPixels、totalPixels、coveragePercent、diffBounds、就绪信号,以及 design、page、heatmap、overlay、带注释的图片和 metrics 的路径。它写入 metrics.json 的同样是这个对象,没有任何有用的东西被丢掉。Agent 读取 matchPercent 知道自己离目标多近,读取 diffBounds 知道下一步在哪里编辑,然后再次调用函数。
要检查整个应用?启动一次 Chromium 并传入每次调用。它是套件或 Agent 反复运行工具时的真实速度提升。
import { designDiff, launchBrowser } from "design-diff"
const browser = await launchBrowser()
try {
for (const page of pages) {
const r = await designDiff({ url: page.url, design: page.design, browser })
console.log(page.url, r.matchPercent)
}
} finally {
await browser.close()
}
Design Diff 比较的是像素,不是感知,所以把分数当作指引而不是判决。抗锯齿和字体渲染在不同机器上有差异,所以真实页面对设计稿导出图即使看起来没问题也往往止步于略低于一百的干净分数。选择一个阈值而不是追逐最后一个点。设计稿和页面尺寸必须一致,因为大的不匹配是设计上的硬性错误。高分数是必要的但不是充分的,因为移位的组件仍然可以藏在里面。差异框和人眼始终是最终的检验。
视觉回归工具将应用与其自身过去对比,捕捉无人预期的变化。Design Diff 将应用与设计稿对比,捕捉交接时真正重要的事——画的和建之间的差距。
它很小巧,不要求你对任何服务效忠。这个领域的大多数工具都是为人看报告而构建的。这一款是为被机器读取并在循环中被执行而构建的,所以 Agent 可以独自将页面推向设计稿,而它仍然交给人一张结束争论的图片。指向你的页面,读取边界框,弥合差距。