Nodepact
by SuperMarioYL
README.md
[English](./README.en.md) | **简体中文**
<div align="center">
[](https://www.npmjs.com/package/nodepact)
[](https://nodejs.org)
[](./LICENSE)
[](https://developer.chrome.com/docs/devtools/protocol)
[](https://modelcontextprotocol.io)
[](./src/oracle/verdict.ts)
[](https://github.com/SuperMarioYL/nodepact/releases)
[](https://github.com/SuperMarioYL/nodepact)
# Nodepact
**让节点自己的渲染差异,成为 coding agent 的验收结论。**
你在自己已打开的 Chrome 里点一个节点、留一句话,Nodepact 把它落成一条可判定的断言;
之后每次 `nodepact check`,返回的是那个节点**前后渲染的差异**加 `PASS` / `FAIL`,
`DONE` 由机器的差异说了算,而不是 agent 的措辞。
<sub>元素锚定纠偏对 · 只连你已开的 Chromium · 判定路径上没有模型 · 断言库与差异图全在本地 <code>.nodepact/</code></sub>
</div>
---
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></svg> 为什么需要 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` 连上去,不经过中间层。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7l5 5l-5 5"/><line x1="12" y1="19" x2="19" y2="19"/></svg> 快速开始(10 分钟)
需要一个 Node 22+ 的环境,和一次 Chrome 的重启。
### 1. 安装
```bash
# 方式 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 打开调试端口
```bash
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
# Linux
google-chrome --remote-debugging-port=9222
```
### 3. 起示例页面(这就是输入)
仓库里带了一个故意做错的设置页:提交按钮和邮箱输入框只隔了 4px。
```bash
python3 -m http.server 5173 --directory examples/demo-app
# 打开 http://localhost:5173
```
源码就在 [`examples/demo-app/index.html`](./examples/demo-app/index.html),改一行 CSS 就能看到判定翻转。
### 4. 挂上去、点选、写批注
```console
$ 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
```bash
# 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 等)时这一步是自动的。
```
```console
$ 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
```bash
nodepact mcp --install # 打印 MCP 客户端配置片段,粘贴到你的 agent 配置里
```
配置好之后,你的 coding agent 自己调 `nodepact.check`,拿到 `DONE` / `NOT_DONE` 与差异产物路径,
自己决定要不要继续改。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4v16l13 -8z"/></svg> 演示
完整录屏:[`docs/demo.cast`](./docs/demo.cast)(asciinema v2 格式,`asciinema play docs/demo.cast` 回放)。
```console
$ 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`](./docs/demo-results.json),README 只引用它:
| 观测项 | 值 |
| --- | --- |
| 锚点 | `#submit`,指纹 `99ceb4`,基线 `68x46` |
| 断言 | `gap(#email, #submit) >= 12px` |
| 修复前 / 修复后实测 | `4px` → `14px` |
| 渲染差异 | `165px` / 共 `7900px` = `ratio 0.0209` |
| 阈值 | `epsilon 0.01`,`stability_floor 0.01`,`capture_margin 16px` |
| 判定 | `NOT_DONE` → `DONE`(退出码 0) |
这些数字来自一次真实运行(`capture_scale = 1`,`devicePixelRatio = 1`)。换个屏幕缩放,裁剪尺寸会变,
判定不会:判定看的是断言实测值与差异占比,不是像素的绝对数量。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="2"/><circle cx="18" cy="5" r="2"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0 -7h-8a3.5 3.5 0 0 1 0 -7h3.5"/></svg> 工作原理
```
你的 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`](./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` 断言要求的那种)就会看不见差异。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3l0 7l6 0l-8 11l0 -7l-6 0l8 -11"/></svg> 断言 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` 里存的是结构化的块,不是上面那行字符串:
```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`](./examples/steer.yaml)。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"/></svg> 配置
`nodepact init` 会写一份 `nodepact.yaml`;[`configs/nodepact.yaml`](./configs/nodepact.yaml) 是同样内容的
带注释副本,所有值就是内置默认值。
| 键 | 默认 | 作用 |
| --- | --- | --- |
| `cdp.host` / `cdp.port` | `127.0.0.1` / `9222` | 连哪个 DevTools 端点 |
| `cdp.url_match` | `null` | 只挂 URL 以它开头的那个页面;`null` 时要求只有一个打开页面 |
| `oracle.epsilon` | `0.01` | 渲染差异达到这个占比才算"节点真的变了",`DONE` 需要它 |
| `oracle.stability_floor` | `0.01` | 节点自重渲染噪声超过它,`check` 报 `UNSTABLE` |
| `oracle.capture_scale` | `1` | 截图 clip 的缩放,1 让裁剪不受屏幕 DPR 影响 |
| `oracle.capture_margin` | `16` | 节点四周多裁的像素,让"只挪位置"的修复也能被看见 |
| `oracle.pixel_threshold` | `0.1` | pixelmatch 的单像素颜色容差 |
| `model.provider` / `model.timeout_ms` | `deepseek` / `20000` | 只用 `annotate` 起草断言时用到 |
| `providers.*` | 见下 | OpenAI 兼容端点表 |
| provider | 模型 | API Key 环境变量 | base_url |
| --- | --- | --- | --- |
| `deepseek` | `deepseek-chat` | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |
| `qwen` | `qwen-plus` | `DASHSCOPE_API_KEY` | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
| `glm` | `glm-5` | `ZHIPUAI_API_KEY` | 空——按你的网关填 |
| `kimi` | `moonshot-v1-auto` | `MOONSHOT_API_KEY` | `https://api.moonshot.cn/v1` |
Key 只在调用时从环境变量读,不落盘。GLM 的 OpenAI 兼容地址随部署不同(bigmodel.cn 与内网网关不一样),
所以不预置。**模型名以各家控制台为准**:这里没有在线校验模型名,写错了会在 `annotate` 时得到一个明确的
HTTP 错误,而 `check` 完全不受影响——判定不依赖模型。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12l5 5l-1.5 1.5a3.536 3.536 0 1 1 -5 -5l1.5 -1.5z"/><path d="M17 12l-5 -5l1.5 -1.5a3.536 3.536 0 1 1 5 5l-1.5 1.5z"/><path d="M3 21l2.5 -2.5"/><path d="M18.5 5.5l2.5 -2.5"/></svg> 接入 coding agent(MCP)
```bash
nodepact mcp --install
```
```json
{
"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。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"/><path d="M9 12l2 2l4 -4"/></svg> 与相邻工具的分工
| 做法 | 它负责的那一半 | 谁说了算 |
| --- | --- | --- |
| 聊天里描述缺陷 / 贴截图 | 把意见传给人或 agent | agent 的措辞 |
| 批注类工具(spotnote 等) | 把批注投递给 agent | agent 的措辞 |
| reticle 一类方案 | 让 agent 自己验证自己 | agent 自己的判据 |
| 整页视觉回归(Playwright / Percy) | CI 里的整页基线快照 | 基线像素 |
| `chrome-devtools-mcp` | 让 agent 读 DOM、截图 | agent 的判断 |
| **Nodepact** | 把**你指的那个节点**的前后渲染差异变成验收信号 | 断言 + 渲染差异(无模型) |
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"/><path d="M9 12l2 2l4 -4"/></svg> 能力与边界(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 原生打包。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"/><path d="M3 10l18 0"/></svg> 付费
**谁先付钱。** 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、全本地、可自托管;上面这些服务是它之上的那一层。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7l6 -3l6 3l6 -3v13l-6 3l-6 -3l-6 3v-13"/><path d="M9 4v13"/><path d="M15 7v13"/></svg> 路线图
**已交付(v0.1)**
- m1:CDP 挂载、浮层点选、基线抓取、`check` 对一条手写断言返回 `PASS` / `FAIL` 加渲染差异;
- m2:断言 DSL + zod 校验;中文批注经国产模型起草断言,人确认后写入;拔掉模型端点判定不变;
- m3:MCP 服务暴露 `nodepact.check` / `nodepact.list`,返回 verdict JSON 与产物路径。
细节见 [`CHANGELOG.md`](./CHANGELOG.md)。
**下一步(按需求排,不承诺日期)**
- 团队版要求的那些:内网模型网关接入、跨项目共享断言库、审计导出;
- 跨字体 / 抗锯齿的差异去噪(如果设计伙伴反馈 `UNSTABLE` 太频繁);
- 团队确认后的节点级验收门禁(现在的 v0.1 不做 CI)。
**不做**:见上面「能力与边界」——那份清单是 v0.1 的契约,不是待办。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 9h8"/><path d="M8 13h6"/><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 4v-4h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/></svg> 反馈
- Bug 与想法:[GitHub Issues](https://github.com/SuperMarioYL/nodepact/issues);
- 国内镜像:[gitee.com](https://gitee.com) 上的同名仓库(同步 GitHub);
- 中文长文会发在掘金 / 少数派,同一段 `docs/demo.cast`;英文侧会发一次 Show HN。
v0.1 的坦白:只连 Chromium、只判定不改码、不做 CI。如果你的场景不在这条线上,直说更有用。
## <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg> 许可
MIT。见 [`LICENSE`](./LICENSE)。
<p align="center"><sub><a href="./LICENSE">MIT</a> © 2026 SuperMarioYL</sub></p>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues