Nodepact
Connects directly to the user's already-running Chrome browser over the Chrome DevTools Protocol (--remote-debugging-port) to anchor UI annotations to specific DOM nodes, capture a baseline screenshot of the node, evaluate assertions (gap between elements, computed styles, or 'changed relative to baseline'), and compute pixel-level render diffs. Exposes nodepact.check and nodepact.list tools so agents can get a machine-verifiable DONE/NOT_DONE/UNSTABLE verdict for a UI fix.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Nodepactcheck if my fix to the submit button spacing passes now"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
English | 简体中文
Nodepact
让节点自己的渲染差异,成为 coding agent 的验收结论。
你在自己已打开的 Chrome 里点一个节点、留一句话,Nodepact 把它落成一条可判定的断言;
之后每次 nodepact check,返回的是那个节点前后渲染的差异加 PASS / FAIL,
DONE 由机器的差异说了算,而不是 agent 的措辞。
元素锚定纠偏对 · 只连你已开的 Chromium · 判定路径上没有模型 · 断言库与差异图全在本地 .nodepact/
为什么需要 Nodepact
你让 coding agent 改一处界面,看到的是一个渲染缺陷:按钮贴着输入框、间距不对、颜色偏了。 现在你只有一种表达方式——在聊天里用文字描述它("设置页那个提交按钮离输入框太近了"), 然后自己再看一遍页面,判断 agent 到底改对没有。
这条回路里有两个洞:
批注没有绑在它说的那个元素上。 agent 只能重新猜你指的是哪个节点。
没有一个机器能读的验收信号。 agent 说"我已经修好了",这句话本身就是"完成"的唯一证据。
于是人既是描述者又是复核者,回路永远合不上。
Nodepact 把这两件事合成一个原语:元素锚定纠偏对(element-anchored steering pair)。
锚点 = 那个 DOM 节点的稳定定位(CSS 选择器 + 结构指纹)+ 修复前的渲染基线(该节点的裁剪截图)。
断言 = 一条可判定的谓词:两节点间距、computed style,或者"相对基线确实变了"。
判定 =
断言成立 ∧ 该节点前后渲染差异 ≥ ε,路径上没有模型。
所以 check 给你的不是措辞,是一组可以复核的数:实测值、像素差异、差异 PNG 和结论。
它补的是哪一半。 把批注投递给 agent 的那一半已经很拥挤了(spotnote 这类 Vite 插件、reticle
这类"让 agent 自证"的方案);Nodepact 不重复那一半,它让你指的那个节点自己的渲染差异成为
验收信号。检查面这一层和 chrome-devtools-mcp(让 agent 读 DOM、截图的那层 agent-to-DOM 能力)
是同一条 CDP 通路,Nodepact 直接用 chrome-remote-interface 连上去,不经过中间层。
Related MCP server: Chrome Ops MCP
快速开始(10 分钟)
需要一个 Node 22+ 的环境,和一次 Chrome 的重启。
1. 安装
# 方式 A:直接装发布包(第一次 tag 推送之后可用)
npm i -g https://github.com/SuperMarioYL/nodepact/releases/latest/download/nodepact.tgz
# 方式 B:从源码(任何时刻都能跑)
git clone https://github.com/SuperMarioYL/nodepact.git && cd nodepact
npm ci && npm run build
npm link # 之后就能直接敲 nodepact发布到 npm registry 之后,npm i -g nodepact 与 npx nodepact 是同一条路径的短写法。
Nodepact 不下载任何浏览器:它只连你已打开的那一个。
2. 让 Chrome 打开调试端口
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
# Linux
google-chrome --remote-debugging-port=92223. 起示例页面(这就是输入)
仓库里带了一个故意做错的设置页:提交按钮和邮箱输入框只隔了 4px。
python3 -m http.server 5173 --directory examples/demo-app
# 打开 http://localhost:5173源码就在 examples/demo-app/index.html,改一行 CSS 就能看到判定翻转。
4. 挂上去、点选、写批注
$ nodepact init --url http://localhost:5173
attached to Chrome via CDP ws://127.0.0.1:9222/devtools/page/4C2E9A7B... page: /
config nodepact.yaml
store .nodepact/
next nodepact annotate
$ nodepact annotate
click a node ...
picked #submit fp 99ceb4
annotation (zh): 提交按钮和上面的邮箱输入框太近了
draft assertion: gap(#email, #submit) >= 12px [y / edit / n]
accept? y
saved to .nodepact/steer.yaml (node fp 99ceb4, baseline 68x46)
next: nodepact check浮层出现在页面右下角:点节点,写下你的意见(中文就行),回车提交。如果你配了模型端点,
annotate 会把这句话起草成一条断言给你确认;没配也能用,直接手写断言。
5. 改一行 CSS,刷新页面,看它翻成 DONE
# examples/demo-app/index.html
# #email { margin-bottom: 4px; } -> #email { margin-bottom: 14px; }
# 然后让页面重新加载。check 读的是你 Chrome 里当前这个页面:
# python3 -m http.server 没有热更新,改完文件页面还是旧的,要在 Chrome 里按 Cmd-R(F5)刷新一次。
# 用带 HMR 的 dev server(Vite、Next 等)时这一步是自动的。$ nodepact check
#submit gap(#email, #submit) >= 12px
before 4 after 4 assertion FAIL
render diff 0px (0.00%) noise 0.00% epsilon 1.00%
artifact .nodepact/diffs/submit.after.png
verdict NOT_DONE assertion does not hold: gap 4px >= 12px -> fail
# ...改完 CSS,并在 Chrome 里刷新页面(Cmd-R / F5)。不刷新的话第二次 check 仍是 NOT_DONE...
$ nodepact check
#submit gap(#email, #submit) >= 12px
before 4 after 14 assertion PASS
render diff 165px (2.09%) noise 0.00% epsilon 1.00%
artifact .nodepact/diffs/submit.after.png
verdict DONE assertion holds and the node changed by 2.09% (≥ epsilon 1.00%)退出码:DONE 是 0,NOT_DONE / UNSTABLE 是 1,锚点或基线出问题是 2。可以直接进你的脚本或 CI。
6. 让 agent 自己循环到 DONE
nodepact mcp --install # 打印 MCP 客户端配置片段,粘贴到你的 agent 配置里配置好之后,你的 coding agent 自己调 nodepact.check,拿到 DONE / NOT_DONE 与差异产物路径,
自己决定要不要继续改。
演示
完整录屏:docs/demo.cast(asciinema v2 格式,asciinema play docs/demo.cast 回放)。
$ nodepact init --url http://localhost:5173
attached to Chrome via CDP ws://127.0.0.1:9222/devtools/page/4C2E9A7B... page: /
$ nodepact annotate
picked #submit fp 99ceb4
annotation (zh): 提交按钮和上面的邮箱输入框太近了
draft assertion: gap(#email, #submit) >= 12px [y / edit / n]
accept? y
saved to .nodepact/steer.yaml (node fp 99ceb4, baseline 68x46)
$ nodepact check --json
{ "verdict": "NOT_DONE", "before": 4, "after": 4,
"render_diff": { "changed_px": 0, "total_px": 7900, "ratio": 0 } }
# agent 改掉那一行 CSS 并让页面重新加载(静态服务器没有 HMR,刷新一下;HMR 的 dev server 自动),再调一次:
$ nodepact check --json
{ "verdict": "DONE", "before": 4, "after": 14,
"render_diff": { "changed_px": 165, "total_px": 7900, "ratio": 0.0209 } }这次运行的全部数字都记录在 docs/demo-results.json,README 只引用它:
观测项 | 值 |
锚点 |
|
断言 |
|
修复前 / 修复后实测 |
|
渲染差异 |
|
阈值 |
|
判定 |
|
这些数字来自一次真实运行(capture_scale = 1,devicePixelRatio = 1)。换个屏幕缩放,裁剪尺寸会变,
判定不会:判定看的是断言实测值与差异占比,不是像素的绝对数量。
工作原理
你的 Chrome(dev server 页面) <--- CDP (--remote-debugging-port=9222)
| ^
overlay.js | | Page.captureScreenshot(clip = 节点框 ± capture_margin)
v | getBoundingClientRect / getComputedStyle
nodepact CLI(node,单进程)
|- anchor/ fingerprint + store -> .nodepact/steer.yaml + .nodepact/baseline/*.png
|- oracle/ assert.eval + diff(pixelmatch) + verdict -> .nodepact/diffs/*.png
|- annotate/ draft -> 国产模型(OpenAI 兼容,仅起草断言,可离线跳过)
\- mcp/ server -> nodepact.check / nodepact.list <-- coding agent最多两个进程:你自己开的 Chrome,和 nodepact 的 CLI / MCP 进程。没有守护进程、没有数据库、
没有云端——全部状态是 .nodepact/ 下的扁平文件。
判定是一个纯函数(src/oracle/verdict.ts):
DONE ⟺ 断言成立 ∧ diff(基线, 现在).ratio ≥ ε
NOT_DONE 其余情况
UNSTABLE 该节点自己重渲染两次的差异 > stability_floor判定里没有模型,所以同一份代码跑两次结论一定一样。锚点也不是选择器:除了 CSS 选择器,它还带一份
结构指纹(标签 + 稳定属性 + 最近三层祖先链 + 文本哈希)。DOM 漂移之后选择器可能失效,指纹还能把节点找回来;
如果两个候选打平,check 直接报 anchor_not_found,绝不猜。
稳定性是这套 oracle 的核心风险,所以它是明说的
动态内容、网页字体、抗锯齿都会让同一个节点的两次截图天然不同。Nodepact 的取舍是:
几何类断言(
gap/style)本身不受抗锯齿影响,它们是主判据;像素差异只作为"这个节点确实被动过"的辅证,阈值 ε 按节点框内像素占比算,且
includeAA: false;每次
check都把实测值、ε、本次噪声一起打出来,让"噪声"和"真修复"可分辨;两次背靠背截图的差异超过
stability_floor时,报UNSTABLE,不假装DONE;截图会在节点四周多裁
capture_margin(默认 16px):裁得太紧的截图会跟着节点一起移动, 一个只把节点挪开的修复(正是gap断言要求的那种)就会看不见差异。
断言 DSL
只有三种,一行一条(nodepact check 接受这种写法,steer.yaml 里存的是等价的块):
gap(#email, #submit) >= 12px # 两个节点的边缘间距,单位 CSS 像素
gap(#email, #submit) <= 12px
style(display) == flex # 锚定节点的 computed style
style(font-size) != 14px
changed() # 相对基线,这个节点确实变了gap 取两个盒子边缘到边缘的距离:上下排布时取垂直间距,左右并排时取水平间距,重叠为 0。
style 读的是锚定节点(annotate 时你点的那个)的 getComputedStyle,属性名用小写 CSS 名。
steer.yaml 里存的是结构化的块,不是上面那行字符串:
version: 1
pairs:
- anchor:
id: submit
selector: "#submit"
fp: button#submit[type=button]|div>form#settings>main|t:b6fc31
baseline_png: .nodepact/baseline/submit.png
box: { x: 431, y: 226.59375, w: 68, h: 46 }
annotation: 提交按钮和上面的邮箱输入框太近了
assertion:
kind: gap
a: "#email"
b: "#submit"
op: ">="
px: 12
baseline_measure: 4
created_at: 2026-10-26T02:41:00.000Z
updated_at: null为什么断言是块而不是一行? YAML 遇到
" #"就结束一个纯量。手写assertion: gap(#email, #submit) >= 12px会被安静地解析成gap(#email,——而手写这个文件正是它的用法。 所以选择器值一律加引号(a: "#email"),断言用块。改断言时记得把baseline_measure置为null, 免得check打出一个过期的 before 值。
完整的示例文件:examples/steer.yaml。
配置
nodepact init 会写一份 nodepact.yaml;configs/nodepact.yaml 是同样内容的
带注释副本,所有值就是内置默认值。
键 | 默认 | 作用 |
|
| 连哪个 DevTools 端点 |
|
| 只挂 URL 以它开头的那个页面; |
|
| 渲染差异达到这个占比才算"节点真的变了", |
|
| 节点自重渲染噪声超过它, |
|
| 截图 clip 的缩放,1 让裁剪不受屏幕 DPR 影响 |
|
| 节点四周多裁的像素,让"只挪位置"的修复也能被看见 |
|
| pixelmatch 的单像素颜色容差 |
|
| 只用 |
| 见下 | OpenAI 兼容端点表 |
provider | 模型 | API Key 环境变量 | base_url |
|
|
|
|
|
|
|
|
|
|
| 空——按你的网关填 |
|
|
|
|
Key 只在调用时从环境变量读,不落盘。GLM 的 OpenAI 兼容地址随部署不同(bigmodel.cn 与内网网关不一样),
所以不预置。模型名以各家控制台为准:这里没有在线校验模型名,写错了会在 annotate 时得到一个明确的
HTTP 错误,而 check 完全不受影响——判定不依赖模型。
接入 coding agent(MCP)
nodepact mcp --install{
"mcpServers": {
"nodepact": {
"command": "npx",
"args": ["-y", "nodepact", "mcp", "--config", "/你的项目/nodepact.yaml", "--store", "/你的项目/.nodepact"]
}
}
}两个工具:
nodepact.check(可选参数node:锚点 id 或选择器;省略则检查全部)——重新渲染并返回DONE/NOT_DONE/UNSTABLE、实测值、像素差异和产物路径。nodepact.list——列出本项目里存的纠偏对(选择器、你的批注、断言、基线路径)。
一个典型的 agent 循环:调 nodepact.check → 拿到 NOT_DONE 和实测值(after: 4,要求 >= 12px)→
改 CSS → 让页面重新加载(HMR 的 dev server 自动,静态服务器要刷新)→ 再调一次 → 拿到 DONE 和差异图。
中间不需要人。因为判定是确定性的,重复调用同一份代码得到同一个答案。
MCP 只是最后一公里:整套东西从 CLI 就能跑,不需要任何 agent。
与相邻工具的分工
做法 | 它负责的那一半 | 谁说了算 |
聊天里描述缺陷 / 贴截图 | 把意见传给人或 agent | agent 的措辞 |
批注类工具(spotnote 等) | 把批注投递给 agent | agent 的措辞 |
reticle 一类方案 | 让 agent 自己验证自己 | agent 自己的判据 |
整页视觉回归(Playwright / Percy) | CI 里的整页基线快照 | 基线像素 |
| 让 agent 读 DOM、截图 | agent 的判断 |
Nodepact | 把你指的那个节点的前后渲染差异变成验收信号 | 断言 + 渲染差异(无模型) |
能力与边界(v0.1)
已经能做的:
init/annotate/check/label/mcp五个命令,一条闭环;三类断言(
gap/style/changed)+ zod 校验 + 结构化存储;锚点指纹重定位,两个候选打平时拒绝猜测;
渲染差异、差异图、噪声与 ε 一起返回;
DONE/NOT_DONE/UNSTABLE三态判定;中文批注 → 断言草稿(国产模型,OpenAI 兼容,可离线跳过);
MCP:
nodepact.check/nodepact.list。
明确不做的(v0.1 的边界,写在这里免得你踩空):
只连 Chromium:CDP 是 Chromium 的协议,Firefox / Safari 不支持;
只判定,不改码:不自动改 DOM、不自动修代码;
不做 CI 与无头视觉回归:基线是你自己那台机器、你那个窗口的截图,所以
.nodepact/被 git 忽略, 换台机器要重新annotate;不做浏览器商店扩展:只有 CLI + MCP + 注入式浮层;
不做 Web 面板 / 云控制台 / 多用户 / 登录 / 团队服务端;
断言不超过三类,不做布局树推理;不做跨页面、多路由的流程断言;
不做 IDE 内嵌插件;不做托管的模型网关与计费;不做 Windows 原生打包。
付费
谁先付钱。 5–50 人的中后台前端 / 研发效能团队:他们在内部 UI 上跑 coding agent, 而断言库、差异图、批注都必须留在自己机器上(数据不出境 / 信创 / 等保)。对他们来说, "节点级验收信号"要落到内网才算数。
他们买的是什么——团队版。
私有化部署支持:把
annotate的断言起草接到你们自己的模型网关(自建 DeepSeek / Qwen / GLM / Kimi), 不走公网;上线陪跑与排障;跨项目共享断言库:一套
steer.yaml约定与评审流程,在多项目、多仓库之间复用;审计留痕:哪条断言、锚在哪个节点、哪一次
check翻成了DONE,可导出;信创适配与开票:内网环境适配、对公转账与发票。
价格。 ¥30k–60k / 团队·年(30–50 席约 ¥600–1200 / 席·年)。前 3 个设计伙伴按 ¥10k 创始价合作, 换一次公开案例研究。
最小的一次"好"。 30 分钟屏幕共享,挂到你们自己的仓库上:你点一个节点、写一句中文批注,
你们 agent 的下一次提交把 check 翻成 DONE,带着差异 PNG。然后才谈"要不要接你们内网的模型网关、
断言库放你们自己机器上"。结算走对公转账 / 开票 + 自托管 license 文件;v0.1 没有云端控制台,
所以在第一张发票之前没有需要先建的东西。
咨询团队版:在仓库开一个 issue,标题写「团队版」。
内核是 MIT、全本地、可自托管;上面这些服务是它之上的那一层。
路线图
已交付(v0.1)
m1:CDP 挂载、浮层点选、基线抓取、
check对一条手写断言返回PASS/FAIL加渲染差异;m2:断言 DSL + zod 校验;中文批注经国产模型起草断言,人确认后写入;拔掉模型端点判定不变;
m3:MCP 服务暴露
nodepact.check/nodepact.list,返回 verdict JSON 与产物路径。
细节见 CHANGELOG.md。
下一步(按需求排,不承诺日期)
团队版要求的那些:内网模型网关接入、跨项目共享断言库、审计导出;
跨字体 / 抗锯齿的差异去噪(如果设计伙伴反馈
UNSTABLE太频繁);团队确认后的节点级验收门禁(现在的 v0.1 不做 CI)。
不做:见上面「能力与边界」——那份清单是 v0.1 的契约,不是待办。
反馈
Bug 与想法:GitHub Issues;
国内镜像:gitee.com 上的同名仓库(同步 GitHub);
中文长文会发在掘金 / 少数派,同一段
docs/demo.cast;英文侧会发一次 Show HN。
v0.1 的坦白:只连 Chromium、只判定不改码、不做 CI。如果你的场景不在这条线上,直说更有用。
许可
MIT。见 LICENSE。
This server cannot be deployed
Maintenance
Related MCP Connectors
Website checker for AI coding agents. Measures your site in a real browser, with evidence.
Browser-backed QA with evidence and fix-ready reports for coding agents.
Visual testing for coding agents: your agent sees every UI change it makes and triages each diff.
Give agents eyes on any web page: structured context, and changes explained in plain language.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables coding agents and CI to verify rendered web UIs against design token usage, layout constraints, and accessibility rules, returning concise pass/fail verdicts with actionable findings.MIT
- AlicenseBqualityBmaintenanceEnables AI coding agents to inspect and operate Chrome as a developer, including reading DevTools console/network data, evaluating JavaScript, and managing unpacked extensions during an edit-test-debug loop.24MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to run an evidence-backed visual QA loop for browser interfaces, including inspecting a running app, retrieving findings with screenshots, and rechecking only the captures affected by a fix.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI coding agents to audit rendered web pages for design and UI defects by measuring geometry, styles, contrast and motion in a local browser and judging them against evidence-tiered rules. Findings come back as short, fixable items expressed in the project's own design tokens, while only sanitized numeric measurements leave the machine.Apache 2.0