内容
# design-compare-mcp
一个MCP服务器,用于衡量**一个UI在多大程度上忠实地再现了另一个UI** —— 参考设计 vs. 实现 —— 并返回一个分数和一个具体的、可行的改进清单。它的存在是为了驱动一个迭代的 *"爬坡式克隆"* 循环:一个代理渲染它的实现,与目标进行比较,修复最大的差异,然后重复。
**网站:** [codenameakshay.github.io/design-compare-mcp](https://codenameakshay.github.io/design-compare-mcp/) — 概述、五个维度、安装步骤和实时的交互演示([源码在 `site/`](site/))。
它在两个通常被忽略的轴上进行比较:
- **静态保真度** (`compare_designs`) — 布局、颜色、内容、排版、间距的静态帧。
- **动态保真度** (`compare_motion` + `capture_frames`) — 动画是否以类似的能量和节奏随时间变化。截图无法看到运动;组件库通常依赖于它。
本文档是一个深入探讨:每个分数的含义、如何计算、如何针对人为判断进行校准,以及 **它的弱点和可能导致的误导**。如果一个数字看起来不正确,[校准](#calibration-is-it-any-good)和[已知限制](#known-limitations--how-it-can-be-wrong)章节的编写是为了让您可以判断是UI还是工具的问题。
参见 [PLAN.md](PLAN.md) 获取原始设计原理,[calibration/README.md](calibration/README.md) 获取校准工作流程。
## 目录
- [理念](#the-idea)
- [安装和注册](#install--register)
- [工具](#the-tools)
- [比较的工作原理(流程)](#how-a-comparison-works-the-pipeline)
- [五个维度](#the-five-dimensions)
- [聚合:加权几何平均值](#aggregation-weighted-geometric-mean)
- [预设和权重](#presets--weights)
- [阅读诊断图像](#reading-the-diagnostic-images)
- [动态比较](#motion-comparison)
- [校准:好不好?](#calibration-is-it-any-good)
- [已知限制和可能导致的错误](#known-limitations--how-it-can-be-wrong)
- [架构](#architecture)
- [强化、确定性和信任模型](#hardening-determinism--trust-model)
- [配置](#configuration)
- [开发](#development)
- [仓库布局](#repo-layout)
## 理念
您有一个 **参考** (设计、网站、组件库)和一个 **候选** (您的实现 — 应用截图、Flutter/React渲染、裁剪的小部件)。您想要一个随着候选接近参考而 **增加** 的数字,以及一个 **修改什么** 的列表来提高它。
核心设计原则:**分数的作用是具有可操作性和单调性,而不是一个单一的“准确”真相。** 比较返回:
- 一个 **`overall`** 0–100,
- 五个 **子分数** (每个0–100,或 `null` 当维度不适用于对时),
- **`cv_findings`** — 修改清单(具体颜色、缺失/额外元素、间距/文本不匹配),
- **诊断图像** 调用模型可以查看,
- 一个 **`critique_rubric`** 告诉主机如何将所有这些转化为优先修复列表。
## 安装和注册
要求:**Node ≥ 20**,**Python ≥ 3.10**,以及(对于 `capture_frames`)本地 **Chrome/Chromium**。
```bash
npm install
npm run build # tsc -> dist/
python3 -m venv worker/.venv
worker/.venv/bin/pip install numpy pillow scikit-image opencv-python-headless scikit-learn scipy
npm run smoke # 端到端检查;以 ALL CHECKS PASSED 结尾 ✅
```
使用Claude Code注册(用户范围 = 在每个项目中可用):
```bash
claude mcp add design-compare --scope user -- node /abs/path/to/design-compare-mcp/dist/index.js
```
在任何更改后重新构建 (`npm run build`) 并重启会话 — 已注册的服务器运行已构建的 `dist/`。
## 工具
四个工具:`ping`,`compare_designs`,`compare_motion`,`capture_frames`。
### `compare_designs` — 静态比较
输入:`reference`(路径),`candidate`(路径),`mode`(`"widget"` | `"screen"`),可选 `preset`(`"default"` | `"dark-ui"`),`weights`,`ignoreRegions`,`returnVisuals`。
示例 — 将参考卡屏幕与重新着色的版本进行比较:
```jsonc
// compare_designs(reference="reference.png", candidate="recolored.png", mode="screen")
{
"overall": 82.4,
"subscores": {
"layout": { "score": 99.97, "reason": "结构相似性(SSIM)对齐后",
"measurements": { "ssim": 0.9997, "alignment": { "method": "ecc_translation", "cc": 1.0, "dx": -0.0, "dy": -0.0 } } },
"color": { "score": 43.95, "reason": "主色调匹配(平均ΔE2000)",
"measurements": { "mean_delta_e": 15.62, "palette_size": [6, 6], "top_deltas": [45.2, 28.6, 18.2, 1.6] } },
"content": { "score": 100.0, "reason": "参考区域覆盖率(IoU匹配)",
"measurements": { "coverage": 1.0, "ref_regions": 8, "cand_regions": 8, "missing": 0, "extra": 0 } },
"typography": { "score": null, "reason": "文本数量 + 规模(粗略;非字体身份)",
"measurements": { "note": "在两个图像中均未检测到文本" } },
"spacing": { "score": 100.0, "reason": "块边距 + 垂直节奏(粗略)" }
},
"cv_findings": [
{ "area": "color", "type": "color_shift", "severity": "high",
"observed": "主色读取为 #dc3c3c 但参考使用 #2878dc(ΔE 45)",
"suggested_fix": "将 #dc3c3c 移向 #2878dc" },
{ "area": "color", "type": "color_shift", "severity": "high",
"observed": "主色读取为 #78283c 但参考使用 #283c78(ΔE 29)",
"suggested_fix": "将 #78283c 移向 #283c78" }
],
"preset": "default",
"weights": { "layout": 0.35, "color": 0.2, "content": 0.15, "typography": 0.15, "spacing": 0.15 }
}
```
阅读它时:**布局和内容是完美的(相同的形状,所有元素都存在),但颜色有很大差异(平均ΔE 15.6,最差对ΔE 45/29)** — 并且发现确切指出哪些十六进制更改为哪些。`typography` 为 `null`,因为此屏幕没有文本;该维度被排除在 `overall` 之外。
发现还定位结构问题。删除候选中的CTA按钮会产生:
```jsonc
{ "area": "content", "type": "missing_region", "severity": "high", "box": [229, 1150, 312, 92],
"observed": "参考区域在x=229,y=1150(312x92)处缺失",
"suggested_fix": "在此位置添加缺失的元素" }
```
### `compare_motion` — 时间比较
输入:`reference`,`candidate`(每个 **帧目录** 或 **帧路径数组** 随时间捕获),可选 `maxFrames`,`returnVisuals`。
```jsonc
// compare_motion(reference="loader/ref_frames", candidate="loader/cand_frames")
{
"ref_energy": 0.00474, // 参考中的运动(每帧平均变化)
"cand_energy": 0.00288, // 候选中的运动
"energy_ratio": 0.607, // 最小/最大 — 它们是否以类似的数量级动画?
"temporal_corr": 0.639, // 它们是否在相同的时间/节奏下移动?(对于稳定运动为null)
"ref_moving": true, "cand_moving": true,
"motion_score": 60.7 // 如果一个动画而另一个是静态的,则为0
}
```
阅读它时:**两者都动画,在相关节奏下,但候选加载器移动量比参考加载器少约40%**。
### `capture_frames` — 获取 `compare_motion` 的帧
输入:`url`,可选 `frames`,`intervalMs`,`clip`(裁剪到预览),`viewport`,`waitMs`,`waitForFlutter`(等待 `<flutter-view>` 挂载),`actions`(有序点击/悬停/等待步骤以导航SPA或触发交互),`outDir`。驱动无头Chrome,写入PNG序列,并返回输出 `dir` 加上第一个/最后一个样本帧以确认捕获不是空白。
### 运动流程 — 完全代理驱动
```
capture_frames(url = reference-component-url, clip = {…}) → dir_ref
capture_frames(url = candidate-url, waitForFlutter = true,
actions = [{click:[x,y], wait:1500}], clip = {…}) → dir_cand
compare_motion(reference = dir_ref, candidate = dir_cand)
```
## 比较的工作原理(流程)
`compare_designs` 运行一个固定的流程。了解阶段是信任(或不信任)分数的关键。
1. **加载** — 将两个图像解码为RGB。输入在解码前会限制大小(参见 [强化](#hardening-determinism--trust-model))。
2. **归一化** — 将两者调整为规范宽度(**768像素**,保留长宽比),然后将较短的调整为相等大小的画布。高度/长宽比差异被记录并作为 `aspect_mismatch` 发现,而不是静默地扭曲分数。
3. **对齐** — 将候选注册到参考:
- `screen` 模式 → ECC转换(`cv2.findTransformECC`),如果不收敛则回退到身份(在诊断中标记);
- `widget` 模式 → 身份(假设紧密裁剪预对齐)。
4. **评估五个维度** — 每个维度独立(如果指标在退化输入上抛出,则仅为 **该维度** 返回 `null`;其他维度仍会计算)。
5. **聚合** — 在已评估的维度上进行加权几何平均。
6. **可视化** — 渲染诊断图像(除非 `returnVisuals:false`)。
**只有 `layout` 使用对齐的候选。** SSIM对1像素的偏移非常敏感,因此全局偏移被注册掉,布局测量 *形状*。其他四个维度故意在 **未对齐** 的候选上运行:颜色/排版是不依赖对齐的,内容/间距必须保持位置敏感,以便真实的位置不匹配作为 *间距/位置* 发现,而不是被静默纠正。这也避免了那些维度被插值伪影污染。
**模式。** `widget` = 紧密裁剪的组件,严格,不对齐。`screen` = 完整视图,宽容,自动对齐。选择 `widget` 时,可以裁剪到确切的组件(最佳信号);选择 `screen` 进行整个页面。
## 五个维度
| 维度 | 默认权重 | 测量 | 下降原因 | 已知弱点 |
|---|---|---|---|---|
| **布局** | 0.35 | 结构相似性 | 形状/结构不同,模糊,对齐失败 | 灰度(对颜色盲);在重度模糊下趋于平稳 |
| **颜色** | 0.20 | 主色调匹配 | 调色板/色调/亮度不同 | 在单一主题UI中几乎无用(无调色板方差);忽略位置 |
| **内容** | 0.15 | 参考块是否存在且位置正确 | 元素缺失/额外/移动 | 粗糙分割;区域框晃动 |
| **排版** | 0.15 | 文本数量 + 规模 | 文本更多/更少,字体更大/更小 | **不** 字体身份;`null` 当没有文本时 |
| **间距** | 0.15 | 边距 + 垂直节奏 | 边距/间隙不同 | 最粗糙;`null` 在稀疏视图上;全宽块将左/右边距归零 |
每个维度的详细信息:
- **布局(结构)。** 对齐的灰度图像上的SSIM(`skimage.metrics.structural_similarity`);窗口限制为 `min(7, h, w)`,以防止薄/小画布出错。分数 = `clamp(SSIM,0,1)×100`。SSIM差异映射也驱动差异热图可视化。
- **颜色。** 确定性k-means(k=6,固定种子)在 **CIELAB** 中从每个图像中提取主色调(下采样到160像素宽)。参考和候选色调通过最小 **ΔE2000** (匈牙利分配)一对一匹配。`mean_delta_e` 是平均匹配的ΔE;分数 = `100 × exp(−mean_ΔE / 19)`(ΔE 0→100,≈13→51,≈25→27)。发现将每个偏移对命名为十六进制。对齐不变性通过设计(全局色调),因此它不说明颜色 **在哪里**。
- **内容存在。** 对比自适应检测器(Otsu阈值亮度增量,从 **模态背景**,**联合** 梯度/边缘掩码)分割主要块,形态闭合和连接组件标记。参考区域通过IoU≥0.3与候选区域匹配;分数 = `100 × coverage × (1 − 0.5·min(1, extra_ratio))`。未匹配的参考区域成为 `missing_region` 发现(带有框);未匹配的候选区域成为 `extra_region`。如果参考没有可检测的块,则返回 `null`。
- **排版。** 通过形态学(梯度 → Otsu → 宽水平关闭 → 宽高比/尺寸过滤)查找文本行 — **不** 是OCR或字体匹配。分数混合文本 *数量* (前景区域分数)和 *规模* (中位线高)作为最小/最大比率。字体系列/权重显式留给主机的视觉模型。`null` 当任一侧没有可检测的文本时。
- **间距。** 仅来自 **主要** 块(比内容更大的最小区域,因此内部文本/细节不会稀释它):外部边距(左/右/上/下,归一化)+块间垂直间隙。每个特征都与容差(维度差异的12% = 满分)进行比较;分数 = 特征相似性的平均值。`null` 当可评估的块太少以至于无法评估节奏时。
## 聚合:加权几何平均值
`overall` 是 **加权几何平均值** 在已评分(非 `null`)维度上,具有在子集上重新归一化的权重,并且每个分数在1.0处取底:
```
overall = exp( Σ (wᵢ / Σw) · ln(clamp(scoreᵢ, 1, 100)) )
```
为什么是几何平均值,而不是算术平均值:**弱维度会大大降低整个分数**,远大于平均值会降低的幅度,因此您 **不能利用一个维度来掩盖另一个破碎的维度**(完美的颜色但结构错误仍然会得到低分)。这在测试套件中通过游戏测试得到验证(输入 `{layout:12, color:100, …}` 得分约为35几何 vs 约56算术)。`null` 维度简单地被排除,因此相同输入的得分约为100,无论是否每个维度都适用。
## 预设和权重
默认权重是一个平衡的起点,**不是** 普遍真理 — 正确的权重取决于领域(参见 [校准](#calibration-is-it-any-good))。
- **`default`** — `layout 0.35, color 0.20, content 0.15, typography 0.15, spacing 0.15`。
- **`dark-ui`** — `layout 0.60, typography 0.30, color 0.10, content 0, spacing 0`。在黑暗、单一主题的组件库移植上进行校准:布局主导,颜色权重降低(无色谱方差),内容/间距关闭。在该数据集上将与人类标签的相关性从~0提高到~0.44。
传递 `preset: "dark-ui"`,或用显式 `weights` 覆盖任何维度(优先于预设)。已解析的预设和权重在每个结果中都会回声以保证透明度。
## 阅读诊断图像
`compare_designs` 返回最多四张图像(作为MCP图像块);`compare_motion` 返回一张:
- **叠加层** — 参考和对齐的候选混合50/50;幻影显示它们在哪里不同。
- **差异热图** — SSIM差异映射;**热点 = 更大的结构差异**。(如果布局指标无法运行,则省略。)
- **内容区域** — 在参考上绘制的检测到的块:**绿色 = 匹配,红色 = 缺失,橙色 = 额外**。这是内容分数背后的可视化。
- **并排** — 参考和候选以匹配大小,直接供人查看。
- **运动签名** — 每帧运动的时间线图表:**绿色 = 参考,蓝色 = 候选**。比较曲线高度(强度)和形状(时序)。
## 动态比较
一张图像无法衡量动画,因此 `compare_motion` 比较两个 **帧序列**:
- **签名** — 对于每个连续的帧对,平均绝对灰度变化(0-1)。随时间变化的序列是组件的 *运动签名*。
- **运动能量** — 签名的平均值:它有多少动画。低于~0.0008的一方是“静态的”。
- **`motion_score`** — 能量比率 `min/max × 100`,即它们是否以类似的数量级动画 — 但 **如果一个动画而另一个是静态的,则为0** (硬性失败)。
- **`temporal_corr`** — 两个签名的皮尔森相关性(它们是否在相同的时间 *移动*);**对于没有时间轮廓的静态运动为null**。
它是 **内容无关的** — 它衡量时间上的变化,而不是外观 — 因此即使源渲染不同的示例内容,它也有效,这是阻止逐帧外观比较的原因。
## 校准:好不好?
待完善。
## 校准:它好用吗?
这是需要批判性阅读的部分。 该工具是针对 **人类标注** 在真实数据集上进行校准的:
26个组件的暗色运动组件库(beUI)与Flutter端口,每个组件由人工评分0-100(盲评,不知晓工具的输出)。相关性采用 **斯皮尔曼相关系数** (该工具是否能像人类一样对移植进行排名)。
| 配置 | 斯皮尔曼相关系数 vs. 人类标注 |
|---|---|
| `默认` 预设,开箱即用 | **-0.03** — 基本无相关性 |
| `默认` 预设,修复暗色UI分割后 | **+0.14** |
| **`暗色UI` 预设** | **+0.44** |
| 最佳可能权重(搜索3个维度) | +0.54 *(过度拟合26个点 — 作为上限,而不是设置)* |
每个维度与人类判断的相关性 **在该数据集上**:
| 维度 | 斯皮尔曼相关系数 | 阅读 |
|---|---|---|
| 布局 | **+0.33** | 最强的单一预测因子 |
| 排版 | **+0.31** | 同样有用 |
| 内容 | +0.23 | 在暗色UI修复后有用(之前为-0.02噪声) |
| 颜色 | +0.03 | **此处无用** — 单个暗色主题,无色调变化可读 |
| 间距 | -0.13 | 弱/噪声在稀疏暗色预览上 |
**这诚实地告诉你:** 在一个暗色、单主题、运动丰富的库上,该工具是一个 *不错的布局+排版检查器* (≈+0.44调优) 而且 **不是** 一个全保真度的神谕。 在一个明亮、多主题、静态UI上,颜色和内容将带来更多的信号 — 这就是按域校准的全部意义。
**为你自己的UI重新校准:** 将`(参考, 候选, 你的0-100标签)`对丢入`校准/`并运行`worker/.venv/bin/python scripts/calibrate.py`。 它报告每个维度的斯皮尔曼相关系数、基线和权重搜索。 15-30对跨质量范围分布可给出有意义的数字; 更少的数据仅有方向性。
## 已知限制及可能出错的情况
在信任任何单个数字之前请阅读此部分。
- **运动上限(最大的限制)。** 静态截图无法看到动画。 在校准集上, **工具与人类之间的8个最大差异都是运动/交互组件**,静态工具 **高估** (例如animated-toast-stack:人类25,工具80;tooltip:50 vs 87)。 如果组件的保真度主要是运动,`compare_designs`看起来太宽容 — 使用`compare_motion`。
- **在单主题中,颜色几乎无用。** 所有的内容都在一个暗色调色板上,没有ΔE可读的方差(rho +0.03)。 不要在这样的UI上权重颜色(`暗色UI`预设将其置低)。
- **内容和间距是粗糙的。** 它们依赖于像素分割而没有DOM。 它们在低对比度暗色UI上 **完全损坏** (直到自适应检测器),它们返回0/`null`; 现在它们有效,但区域框仍然不稳定,间距在稀疏预览上变为`null`。
- **排版是数量+缩放,而不是字体。** 它不会注意到错误的字体家族或权重 — 那故意留给视觉模型。 两个不同字体在相同大小和密度下得分高。
- **`capture_frames`下的单次动画。** 帧在加载后以固定节奏采样,因此计数器或文本显示在采样前完成读取为*静态* — 低`motion_score`可能意味着“未捕获”,而不是“不忠实”。 连续动画(旋转器、字幕、着色器)正常工作。
- **Flutter/CanvasKit在无头捕获。** CanvasKit渲染可能截图黑色或在离屏浏览器中无法启动; DOM/HTML渲染器可靠捕获。 如果帧为空,则是捕获问题,而不是零运动。
- **相关性数字本身。** +0.44是 *中等* 且在n=26上测量的 — 有意义但不权威。 该工具是一个快速、可重复的 **第一步筛选和检查清单生成器** ,而不是人类最终判断的替代品。
- **不同的示例内容。** 如果两个源显示不同的样本数据(`select`显示“Next.js” vs “Pick a fruit”),则静态比较部分是不同类型的比较。 `compare_motion`与内容无关且不受影响。
## 架构
```
主机代理 ──MCP/stdio──► TS服务器 ──spawn once──► Python视觉工作器(持久)
│
└── capture_frames → 无头Chrome(puppeteer-core)
```
- **TS服务器** (`src/`) 通过stdio进行MCP通信,注册工具,直接驱动`capture_frames`(浏览器工作是Node问题),并监督Python工作器。
- **Python工作器** (`worker/vision_worker/`) 执行所有视觉工作(OpenCV,scikit-image,scikit-learn)。 它 **常驻** 以便重载项仅加载一次,而不是每次调用。 TS ↔ Python通过换行符分隔的JSON通信; 工作器的stdout是一个干净的协议通道,所有诊断信息都发送到stderr。
## 强化、确定性和信任模型
- **确定性分数。** 相同的输入 → 相同的输出(k-means已 seeded; 指标中没有随机性)。 这使得分数可以安全地进行爬山优化。
- **监督工作器。** 如果Python工作器崩溃(OOM,本机崩溃),下一个请求将懒惰地重新启动它,并带有延迟重试; 先前健康的崩溃触发一次幂等重试。 一次崩溃仅导致一次调用失败,而不是整个会话。
- **每个指标的隔离。** 某个维度在退化输入上抛出返回`null`(错误在`measurements`中); 其他维度和`overall`仍然计算。
- **输入上限。** 大于~40 MP / 12000像素/边的图像在解码前被拒绝(绑定内存); PIL的解压炸弹防护是一个备用措施; 非图像和错误路径返回键入错误。
- **协议隔离。** 工作器保留实际的stdout文件描述符用于框架JSON,并将`sys.stdout`/fd 1重新指向stderr,以便流氓库输出永远不会破坏通道。
- **信任模型。** 工具读取本地图像路径并(对于`capture_frames`)导航到您给他们的URL; 一个本地单用户假设。 它们永远不会返回原始文件字节(非图像错误输出)。 对于不受信任/多租户主机,请设置`DESIGN_COMPARE_ALLOWED_ROOTS`以限制文件读取。
## 配置
- `DESIGN_COMPARE_PYTHON` — 工作器的Python解释器。 默认情况下,如果存在`worker/.venv/bin/python`,否则为`python3`。
- `DESIGN_COMPARE_CHROME` — `capture_frames`的Chrome/Chromium可执行文件。 如果未设置,则在macOS/Linux/Windows上自动检测。
- `DESIGN_COMPARE_ALLOWED_ROOTS` — 可选的`:`分隔的目录; 设置后,拒绝读取这些目录之外的文件(解析符号链接后)。 未设置 = 任意本地路径。
## 开发
```bash
npm run build # tsc -> dist/
npm run smoke # 端到端MCP客户端:所有四个工具
npm run resilience # 工作器崩溃/重启 + 并发
worker/.venv/bin/python worker/tests/test_pipeline.py # 每个维度行为,单调性
worker/.venv/bin/python worker/tests/test_calibration.py # 单调性,抗干扰性,确定性
worker/.venv/bin/python worker/tests/test_robustness.py # 敌对/退化输入
worker/.venv/bin/python worker/tests/test_motion.py # 运动库 + compare_motion
worker/.venv/bin/python scripts/stress.py # 泄漏 + 延迟探测
```
校准助手:`scripts/capture_pairs.mjs`(捕获参考/候选对),`scripts/build_contact_sheet.py`(盲标记表),`scripts/calibrate.py`(分数标签 + 调整权重),`scripts/capture_frames.mjs` + `scripts/motion_score.py`(运动)。
## 仓库布局
```
src/ # TypeScript MCP服务器
index.ts # 工具注册(ping,compare_designs,compare_motion,capture_frames)
worker-client.ts # 监督 + 多路复用Python工作器
capture.ts # 无头Chrome帧捕获(capture_frames)
schema.ts # zod输入模式
worker/vision_worker/ # Python视觉工作器
pipeline.py # 比较 / 比较数组 / 运动协调
normalize.py align.py # 规范大小 + 注册
metrics/ # 结构,颜色,内容,排版,间距
aggregate.py # 加权几何平均值 + 预设
motion.py # 运动签名 + 序列比较
visuals.py io_utils.py # 诊断图像 + 图像/帧加载
scripts/ # 校准 + 运动 + 恢复/压力助手
calibration/ # 数据集清单,标记表,(gitignored)捕获对
worker/tests/ # pytest-free断言脚本(直接运行)
PLAN.md # 原始设计 + 阶段计划
```
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器