关于 triage-visual-changes
triage-visual-changes 是一项通过 UI Verify 的 MCP 服务审查构建结果的 Agent 技能。它指导检查构建和故事、比较图像、查看评论,并记录视觉变更的处理决定。
主要功能
- 01检查构建门禁和故事变更
- 02比较基线图像与候选图像
- 03查看 AI 判定和人工评论
- 04记录决定并管理评论
- 05汇总 PR 范围的视觉变更
适用场景
- 01分诊构建中的变更故事
- 02在 PR 审查中识别视觉回归
- 03接受变更前检查审查评论
- 04将 PR 累积变更与基线比较
MCP 相关
它将 MCP 客户端连接到 `https://uiverify.ai/mcp` 上的远程 Streamable-HTTP 服务。该服务提供构建、故事、图像、评论和审查决定相关工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uiverify/ uiverify --skill triage-visual-changes安装后在 Claude Code 中输入/triage-visual-changes确认已加载。
详细介绍
在 Agent 中处理 UI Verify 构建的审查
当构建结果为 changed 时,仪表板会为每个故事显示一组三联图(基线 / 候选 / 差异)。此技能可让你通过 UI Verify MCP 在 Agent 中完成审查:查看变更内容,在数值含义不明确时查看像素图,然后总结或接受。
连接 MCP(仅需一次)
该服务是一个远程 Streamable-HTTP MCP,地址为 https://uiverify.ai/mcp。你只需在浏览器中使用 UI Verify 账号登录一次(OAuth),这一个连接便可访问你所属所有团队中的所有项目,因此无需为每个项目单独设置,也无需在配置中填写 API 密钥。请将其作为真正的 MCP 服务添加到 Agent 中,不要自行编写 curl 命令调用端点。 使用原始 curl 时,图像工具会返回 MCP 图像块,Shell 无法渲染,因此脚本无法利用这些像素;原生客户端则能直接将图像显示给模型。
已安装 UI Verify 插件的 Claude Code:插件已连接此服务。运行 /mcp,选择 UI Verify 服务并登录;无需执行下方的命令。
Claude Code — 在项目根目录运行:
claude mcp add --scope project --transport http uiverify https://uiverify.ai/mcp
然后重启 Claude Code,运行 /mcp,选择 uiverify 并登录。--scope project 会将服务写入已提交的 .mcp.json;其中不包含机密,因此整个团队都能使用,每位团队成员使用自己的账号登录即可。
Cursor — 将配置添加到 .cursor/mcp.json(提交该文件即可共享),然后在 Cursor 的 MCP 设置中登录:
{
"mcpServers": {
"uiverify": { "url": "https://uiverify.ai/mcp" }
}
}
其他任何 MCP 客户端(VS Code、Codex、Copilot、Gemini):将其指向同一个 Streamable-HTTP URL,然后按提示登录。添加服务后,重启 Agent 会话,以便加载工具。如果客户端只能手动通过 HTTP 访问,请优先使用 get_diff 工具(它会返回可下载的图像 URL),而不是 render_diff_image(内嵌像素图)——请参阅下方的工具表。
它提供的工具:
| 工具 | 功能 | 关键参数 |
|---|---|---|
list_projects | 列出你可访问的所有项目,包括其 projectId、团队、名称、类型、仓库和最近构建时间。只有在你可访问多个项目时才需要:commitSha / prNumber 查询和 list_builds 随后都需要提供 projectId(只有一个项目时会自动使用;调用存在歧义时会返回错误并列出这些项目) | 无 |
list_builds | 列出近期构建及门禁状态,以找到要检查的构建 | projectId?、branch?、status?、limit? |
get_build | 对单个构建进行精简排查——包括门禁、counts {total, changed, failed, unchanged}、整个构建的评论统计 comments {total, unresolved}、AI 统计,以及已更改 / 失败故事的第一页(25 条)(每条包含一个 diffResultId、差异百分比;启用 AI 审查时,还包含评审器的 aiVerdict、aiConfidence,以及回归项的 aiFlagReason=一句话说明“哪些内容看起来并非有意更改”);页面会附带一个 nextCursor。它不会列出未更改项——counts.unchanged 会提示其数量;使用 list_build_stories 翻页查看其余内容。comments.unresolved > 0 表示有人类 / Agent 留下了未解决的备注——在接受前先用 list_comments 阅读 | 以下之一:commitSha / prNumber / buildId |
list_build_stories | 按状态分页浏览单个构建中的故事——这是查看 get_build 首页之外所有已更改 / 失败 / 未更改项的唯一方式。get_build 返回的 counts.unchanged 是可分页获取的确切数量。每条已更改故事的记录(此处和 get_build 中)都包含自己的 comments {total, unresolved} | 选择器,status: changed | unchanged | failed、cursor?、limit?(默认 25,最大 100) |
get_diff | 获取单个故事的差异指标和预签名图片 URL(基线 / 候选版本 / 差异),可将其下载到文件,或直接链接到 PR 中;启用 AI 审查时,还包含评审器的完整评估:aiVerdict、aiConfidence、aiSummary(更改内容)、aiReasoning(原因)、aiFlagReason;以及单个故事的 comments {total, unresolved}(与 get_build 中相同的提示信息,但仅针对该故事)。传入 storyId 可获取单个故事,即使它没有发生变化——你会得到其当前基线 URL,因此可以在通过的构建中显示“与基线完全相同” | 相同的选择器,可选 storyId |
render_diff_image | 内联呈现单张图片的实际像素,供视觉模型查看(不是 URL):baseline / candidate / diff;或使用 before_after,显示放大到更改区域的前后裁剪图——当故事在多个相距较远的位置发生变化时(例如页眉和页脚),会按区域各生成一张裁剪图并堆叠显示。如果故事有未解决的审查评论,会在图片之前显示一条说明文字 | diffResultId(或未发生变化的故事所用的 storyId)、which |
get_pr_changeset | 获取 PR 与基线之间的累计变更集:整个 pull request 相对于其合并目标分支对 UI 所做的更改——包括 new / changed / removed 故事,每个故事都带有审查状态(启用 AI 审查时还包含评审器的判定),以及 counts {new, changed, removed, unchanged} 和每个列表的第一页(25 条)。不同于 get_build,后者代表单个提交的门禁:此结果在 PR 内接受更改后以及合并后仍然有效,并能捕获先添加后接受的故事,以及任何单个构建差异都无法显示的已移除故事 | 以下之一:commitSha / prNumber / buildId |
list_pr_stories | 分页查看 get_pr_changeset 首页之外的 PR 与基线变更集中的一个类别 | 选择器,kind: new | changed | removed、cursor?、limit? |
review_diff | 记录对单个故事的决定。如果故事有未解决的审查评论,拒绝执行 accept 和 ignore;deny 始终可用 | diffResultId、decision: accept | deny | ignore |
accept_build | 一次性接受构建中的所有已更改故事。如果任何已更改故事有未解决的审查评论,则会拒绝执行且不接受任何内容——错误会列出这些故事 | 相同的选择器 |
list_comments | 阅读设计师 / QA(或其他 Agent)在构建差异上留下的审查评论——构建中的所有未删除评论,每条都包含其 diffResultId、authorType、body、side、parentId(回复所属的讨论串根评论)、anchor(图片上的点 / 区域)和 resolved。这是 get_build / get_diff 中 comments 计数所指向的内容 | 以下之一:commitSha / prNumber / buildId |
post_comment | 在单个故事的差异上留下评论(或带讨论串的回复)——例如回复审查者的问题,或向他们指出回归问题。评论作者为 Agent;如果分支上存在更新的构建,则不允许添加新的根评论(请在最新构建上评论),但始终允许回复 | 选择器 + diffResultId、body、side?、anchor?、parentId?(回复某个根评论的 id) |
resolve_comment | 备注处理完毕后,解决(或重新打开)评论讨论串 | commentId(讨论串根评论)、resolved |
update_comment / delete_comment | 编辑或删除评论——Agent 只能操作由 Agent 撰写的评论 | commentId(更新时还需提供 body) |
配方 1 —“对这次构建进行分诊”
将变更分到不同类别,让人看到的是关键信息,而不是 60 行内容。针对这个 PR 调用 get_build——它会提供第一页变更中每个故事的 AI 裁判结论(aiVerdict;如果是回归,还会提供 aiFlagReason);如果 counts.changed 大于该页的数量,就用 nextCursor 持续分页调用 list_build_stories { status: "changed" },直到没有更多结果,避免回归漏过分页边界。先阅读裁判结论,再形成自己的判断;对于任何确实发生了内容变化的故事,都要对照变更前的图像进行评判,而不是孤立地看变更后的图像:
-
渲染两张图,而不只是差异图。 使用
render_diff_image,分别指定which: "baseline"和which: "candidate",然后进行比较。绿色差异蒙版会告诉你像素在哪里移动了,却不会告诉你结果好不好——深色背景上的代码块、失去对比度的文字、被裁切的控件,在蒙版中看起来都只是“有些像素变了”,只有对比前后图像才能看出这些回归。只看候选图像,就会让“现在有颜色了 → 有改进”这样的判断放过一个无法辨认的代码块。对于被裁判标记的问题,调用get_diff查看裁判的aiReasoning/aiFlagReason。 -
先阅读人的评论,再做决定。 如果
get_build的comments.unresolved > 0(或某个get_diff故事显示了comments),就说明设计师、QA 或其他 Agent 在这次构建中留下了备注——list_comments可以读取这些备注,每条备注都关联到其diffResultId,以便你将备注与分类中的故事对应起来。像素无法体现备注中的上下文(“这个间距是有意为之,请勿还原”/“这里的 logo 不对”),所以在对故事分类之前要先阅读备注。无论如何,你都不能直接接受带有未解决评论的故事:accept_build和review_diff的接受/忽略操作都会拒绝处理带有未解决评论的故事。处理流程是:阅读备注 → 按备注要求处理(通常需要修改代码并重新推送)→resolve_comment→ 接受。绝不要只为解锁接受操作而解决备注;只有在确实处理了备注所述问题后,才解决它。
使用以下四个类别报告,并列出真实的故事名称和变化百分比:
- 回归 — 确实发生了错误的变更 — 内容发生了变化,但变更后的结果有问题或更糟:文字无法辨认/对比度过低、内容被裁切、发生重叠、内容缺失,或控件看起来不再像原来的控件。当前 PR 导致的回归仍然是回归——“我的差异能解释它”并不能说明它是正确的。绝不要接受这些变更;这正是进行分诊的原因。
- 预期变更 — 内容发生了变化,且变更后的结果正确,符合 PR 声明的意图(刻意调整样式、新文案、真正新增的故事)。这些变更仍需要人工检查,但可以考虑接受。
- 外观重排 — 确实有差异像素,但差异图显示的是相同内容仅移动了几个像素(例如某一行下移约 5 px、文字的影子向下偏移)。这不属于内容变更。
- 渲染噪声 — 可安全接受 — 图表边缘/抗锯齿线条上低于 0.4% 的抖动。纯粹的渲染抖动。
裁判给出的 regression 结论意味着要仔细检查,绝不意味着可以置之不理。 如果你打算把裁判标记的故事归为“反正是预期变更”,就必须从变更前后的图像中找到具体理由,反驳其 aiFlagReason,而不是说“只是颜色变了,应该没问题”。无法反驳时,就相信它。
示例输出格式:
67 个发生变化的故事 → 分类。 ⚠️ 回归(2) — 看起来不对的真实变化,请勿接受:
SetupWizard/Playwright(8.3%, 代码块现在是深色背景配深色文字/无法辨认——评审判定为regression),Card/Compact(5.1%,标题 被截断)…… 1. 预期变化(11) — 内容有变化,看起来正确,可作为接受候选:Hero/Default(14%, 新标题文案),…… 2. 视觉重排(9) — 像素确有变化,但仅是垂直位移:EventStatusCard/All Variants(4.1%, 行偏移约 5px),TestingInit/Playground(3.7%),…… 3. 图表抗锯齿噪声(6) — 边缘抖动低于 0.4%,可安全接受:FunnelChart/Smaller Screen(3.2%),UpliftChart(单个 1px 标记),…… 新故事(12) — 首次建立基线,没有可比较的内容:Button/AllVariants,……
切勿在分类步骤中接受任何内容——展示分类结果,由人工决定;或者在配方 3 中明确操作。
配方 2 —“总结视觉回归”
读取方式相同,但只输出需要人工处理的变化——先列出 ⚠️ 回归,再列出预期内容变化——采用 PR 评论格式,每项一行,不包含重排/噪声。将此评论发布到 PR:
🔎 视觉评审 — 1 项回归,2 项预期变化(共 67 项标记;64 项重排/噪声,见下方折叠部分)
- ⚠️
SetupWizard/Playwright— 代码块深色背景配深色文字,无法辨认(8.3%,评审判定:回归)Pricing/Card— CTA 按钮颜色从蓝色变为绿色(12.4%)Nav/Header— 徽标放大 8px(3.1%)
配方 3 —“接受所有新基线”
如果所有变化都是预期的(有意进行的重新设计),或全是噪声,且你已确认列表中没有隐藏的回归(⚠️ 分类为空),即可通过一次调用更新每个发生变化的故事的基线:
accept_build { prNumber: 123 }
不要因为“标题变化是预期的”,就认为整个构建中的变化都理所当然——全局 CSS 或主题更改会波及 PR 原本无意修改的故事;“这是 PR 导致的”不等于“这是 PR 预期的”。每个发生变化的故事都应根据其自身的前后对比单独分类。
如果要有选择地接受(部分变化是真实的,部分不是),请针对每个 diffResultId 循环调用 review_diff,使用 accept/ignore/deny——接受预期变化,忽略噪声,将真实回归留给人工处理。
配方 4 —“将前后对比放进 PR”
让人工直接在 PR 中查看,而不是让他们去仪表板。完成分类后,将发生变化的故事内嵌到 PR 中:
- 最快的单张图片:
render_diff_image的which: "before_after"会生成一张 PNG,将前后版本并排展示,并裁剪到发生变化的区域(如果故事在多个相距较远的位置发生变化,则每个区域各裁剪一张并上下排列)——这是最省力的查看方式。保存图片并附上。 - 可下载文件/持久链接:
get_diff会返回预签名的baselineUrl/candidateUrl/diffUrl。下载这些文件(curl -L "$url" -o before.png)并附到 PR,或者在评论中附上构建链接和这些 URL。务必附上构建链接(https://uiverify.ai/builds/<id>),方便评审者查看完整的三联图。
一条好的 PR 评论应包含:配方 2 中逐项列出的回归摘要、回归项的前后对比图片,以及构建链接。直接发布评论,不要让他们自己去找。
配方 5——“这个 PR 整体对 UI 做了什么?”
get_build 回答的是单个提交的问题——此构建中是否有尚未审查的内容。它无法证明
整个 PR 实现了你的预期,而且从设计上说,它也看不到两种情况:你在 PR 较早阶段添加并接受的
故事(最新构建渲染正常,因此不会显示为已更改),以及已移除的故事(已删除或重命名的基线——没有构建会渲染它,因此不会有任何单次构建差异将其标出)。
只有 get_pr_changeset 能捕捉到这两种情况,所以在你宣布 PR 完成前,请先运行它。
获取该 PR 的 changeset,查看完整的 head 与 base 对比结果:
get_pr_changeset { prNumber: 123 } → new / changed / removed vs the base branch
list_pr_stories { prNumber: 123, kind: "removed" } → page any one bucket
对每一项都用同一标准来判断:你能将它归因于此 PR 的 diff 实际做了什么吗?
- 能,而且这是正确的 → 这是 PR 预期的视觉影响。
- 无法归因于你自己的 diff 的更改 → 停下。不要接受,不要把它当作偶发问题忽略,也不要“重新运行看看”。说明情况并标记给人工处理——可能的原因是基线在不该变动时发生了变化,而这从设计上说不会发出提示。
- 你不希望出现的
removed故事 → 这是意外删除(或非预期重命名,表现为一项删除加一项新增)。请明确标出,不要让它带着绿色勾选通过。
changeset 已正式推出(GA)——无需启用任何标记;对于公开项目,任何人都可以在不登录的情况下查看构建的 changeset。
配方 6——“回复或解决审查评论”
如果审查者的留言是一个你能回答的问题,或指出了你刚刚修复的问题,就由 Agent 直接跟进,而不是让对方再来找你。使用 list_comments 读取讨论串,然后:
- 在讨论串中回复——使用
post_comment { diffResultId, body, parentId },其中parentId是根评论的id(回复会继承根评论所在的一侧,无需单独设置锚点)。用它说明你做出的 diff 如何回答“为什么这里移动了?”,或确认“已在下一次推送中修复”。 - 标记回归问题——使用
post_comment { diffResultId, body, anchor? }发布新的根评论,指出你留给审查者处理的故事(配合配方 1 中的 ⚠️ 分类使用)。 - 解决——留言得到处理后(你已按要求修改,或已回答问题),使用
resolve_comment { commentId, resolved: true }。请在接受前先解决评论:存在未解决的讨论串时,accept_build/review_diff会拒绝该故事。只有在讨论确实结束时才使用此操作;未解决的评论会阻止过早接受。
你的密钥会以 Agent 身份发布评论,且只能编辑或删除自己发布的评论——绝不要触碰人工审查者的评论。
构建通过了?你仍然可以查看像素
没有更改的构建不会显示三联图——但组件仍然经过渲染,而“没有变化”只有在你能看到时才值得信任。使用 list_build_stories { status: "unchanged" } 枚举未更改的故事(根据返回的 nextCursor 翻页),然后按 ID 获取任一故事当前的图像:
用 get_diff { buildId, storyId } 获取其基线 URL,或用 render_diff_image { storyId, which: "baseline" }
查看像素。你可以借此确认绿色构建中的组件显示正确,或在无需打开仪表板的情况下告诉审查者“看这里,它与基线完全一致”。
防护措施
- 先阅读,再操作。 在调用
review_diff/accept_build之前,始终先调用get_build(对有歧义的内容,还要调用render_diff_image)。不要接受尚未查看过的构建。 - 比较前后,而非只看修改后。 对于内容有变更的任何故事,都要分别渲染
which: "baseline"和which: "candidate"并查看两者——差异蒙版会显示变更位置,而两者的对比能看出情况是否变差(对比度 / 可读性、裁切、布局)。「有颜色了 / 移动了」不等于「变好了」。 - 默认应相信 AI 评审器给出的
regression信号。 要推翻它,必须根据修改前后的具体情况,说明其aiFlagReason为何不成立;不能含糊其辞。问题由这个 PR 导致,并不是忽略它的理由。 - 批量接受会更改基线。
accept_build会将候选版本设为所有变更故事的新基准——只有确认列表中没有隐藏的真实回归时,才应使用它。 - 未解决的评论会阻止接受。
get_build中的comments.unresolved > 0表示有人留下了你尚未阅读的备注——先通过list_comments查看并妥善处理,然后再调用review_diff/accept_build。评审者评论「不要接受这个」,而 Agent 仍然批量接受,正是这个计数旨在防止的情况。 - 区分「新故事」和「变更过的故事」。 首次建立基线的故事没有可供比较的差异;这不算回归,只需建立基线。请将其单独归类。