- d8d-dev: 扁平文件 → modes/(journey, validation, visual-diff), phases/, rules/ 子目录 - d8d-dev: journey-mode.md 拆分为 journey-index/definition/execution/validation 四文件 - d8d-skills-sync: 新增技能同步工具 - d8d-iterate: 新增需求迭代编排技能
21 KiB
原型对比验证模式(Visual Diff Mode)
定位:在回归验证和旅程验证之后,将实际页面实现与原型做逐页视觉对比,发现布局偏差、组件遗漏、交互方式差异等视觉问题。
两种对比方式(任选其一):
方式 来源 适用场景 Pencil MCP 直接版(推荐) .pen设计源文件有 .pen文件时首选,100% 设计稿保真度HTML 原型版 prototype/pages/*.html无 .pen文件或需要 HTML 交互时Pencil MCP 直接版文档:visual-diff-mode-pencil.md
与其他模式的关系:
- 回归验证模式 → 检查功能点存在且正确(类比单元测试)
- 旅程验证模式 → 检查用户流程完整性(类比 E2E 测试)
- 原型对比验证模式 → 检查视觉还原度(类比视觉回归测试)
触发时机:
- 旅程验证模式完成后建议进入
- 手动触发:
/d8d-dev visual-diff或/d8d-dev visual-diff epic-3- 流水线自动触发:每个 Epic 完成后(可配置)
前置条件
检查清单(任一不满足 → 报错并终止):
├── prototype/ 目录存在且含 pages/ 子目录
├── prototype/server.js 存在(原型预览服务)
├── pipeline-context.md 存在(含实际应用 URL 和凭据)
├── pm2 服务运行中(实际应用可访问)
└── 至少有一个 Epic 的 Story 已 verified
核心流程
Phase VD-0: 准备
├── 发现原型页面 → 建立页面清单
├── 读取 pipeline-context.md → 获取实际应用 URL
├── 启动原型服务 → node prototype/server.js (port 8080)
└── 构建页面映射表(原型页面 ↔ 实际路由)
Phase VD-1: 逐页对比循环
对每个映射页面:
├── A. 截取原型页面截图
├── B. 截取实际页面截图
├── C. UI 差异对比(三层对比法)
└── D. 差异分级 + 记录
Phase VD-2: 汇总报告
├── 生成 visual-diff-report.md
└── 输出差异统计和修复建议
Phase VD-0: 准备
0.1 发现原型页面
# 列出所有原型页面
find prototype/pages -name "*.html" | sort
从输出中提取页面清单,分类为 frontend 和 admin。
0.2 读取实际应用信息
读取 _bmad-output/implementation-artifacts/pipeline-context.md 获取:
- 实际应用 URL(API_URL, ADMIN_URL)
- 认证凭据
- 已实现的路由
0.3 启动原型服务
# 检查 8080 端口是否已被占用
lsof -i :8080 || true
# 启动原型服务(后台运行)
cd prototype && node server.js &
# 验证服务可用
curl -sf --max-time 5 http://localhost:8080/ && echo "OK"
如果 8080 端口被占用,使用其他端口并记录。
0.4 构建页面映射表
自动映射规则:
对每个原型页面 prototype/pages/{group}/{name}.html:
1. 提取 group (app/admin) 和 name
2. 在实际路由中查找匹配:
- name=login → /login 或 /admin/login
- name=dashboard → / 或 /dashboard
- name=register → /register
- 其他 → /{name} 或 /admin/{name}
3. 验证实际路由可访问(curl 检查非 404)
4. 不可访问的页面标记为 skipped
映射输出格式(记录到 pipeline-state.yaml):
visual_diff_mapping:
- prototype: "pages/app/login.html"
actual_route: "/login"
group: "frontend"
status: "active" # active / skipped / error
- prototype: "pages/app/dashboard.html"
actual_route: "/"
group: "frontend"
status: "active"
Phase VD-1: 逐页对比循环
对映射表中每个 status: active 的页面执行以下流程。
1.0 创建截图目录
mkdir -p tests/e2e/screenshots/visual-diff
1.A 截取原型页面截图
1. browser_navigate → http://localhost:8080/pages/{group}/{name}.html
2. browser_resize → 1440x900 (桌面端) 或 375x812 (移动端)
3. browser_wait_for → time=2 (等待页面渲染)
4. browser_take_screenshot(fullPage=false)
→ tests/e2e/screenshots/visual-diff/{name}-prototype-viewport.png
5. browser_take_screenshot(fullPage=true)
→ tests/e2e/screenshots/visual-diff/{name}-prototype-full.png
1.B 截取实际页面截图
1. browser_navigate → {实际 URL}{route}
2. browser_resize → 同上视口(必须与原型一致)
3. 处理认证:
- 如果页面需要登录 → 从 pipeline-context.md 读取凭据
- browser_navigate → 登录页 → 填写凭据 → 提交 → 等待跳转
- 如果已登录(直接访问无跳转)→ 继续
4. browser_wait_for → time=3 (等待数据加载)
5. browser_take_screenshot(fullPage=false)
→ tests/e2e/screenshots/visual-diff/{name}-actual-viewport.png
6. browser_take_screenshot(fullPage=true)
→ tests/e2e/screenshots/visual-diff/{name}-actual-full.png
1.C 三层对比法
判断页面长度:
// browser_evaluate
() => {
return {
pageHeight: document.documentElement.scrollHeight,
viewportHeight: window.innerHeight,
isLongPage: document.documentElement.scrollHeight > window.innerHeight * 1.5
};
}
如果 isLongPage === false(短页面):
直接对比首屏:
ui_diff_check(
expected_image_source = "{name}-prototype-viewport.png",
actual_image_source = "{name}-actual-viewport.png",
prompt = "对比这两张截图:第一张是设计原型,第二张是实际实现。列出所有视觉差异,包括布局结构、组件排列、颜色、间距、字体、图标等。按严重程度排序。"
)
如果 isLongPage === true(长页面):
执行三层对比法:
层 1:首屏对比(viewport 截图)
ui_diff_check(
expected_image_source = "{name}-prototype-viewport.png",
actual_image_source = "{name}-actual-viewport.png",
prompt = "对比这两张首屏截图:第一张是设计原型,第二张是实际实现。首屏是最重要的视觉区域。列出所有差异,按严重程度排序。"
)
层 2:全页面对比(fullPage 截图)
ui_diff_check(
expected_image_source = "{name}-prototype-full.png",
actual_image_source = "{name}-actual-full.png",
prompt = "对比这两张完整页面截图:第一张是设计原型,第二张是实际实现。注意页面可能很长,重点关注各区域的布局和组件差异。"
)
层 3:差异区域精确定位(仅当层 1 或层 2 发现差异时执行)
对原型和实际页面分别执行分段截图:
1. 计算分段数量 = ceil(pageHeight / viewportHeight)
2. 对每一段:
a. browser_evaluate → window.scrollTo(0, segmentIndex * viewportHeight * 0.8)
// 20% 重叠避免切割区域遗漏
b. browser_wait_for → time=1
c. browser_take_screenshot(fullPage=false)
→ {name}-prototype-section-{N}.png
→ {name}-actual-section-{N}.png
3. 逐段 ui_diff_check 对比
原型页面也需要 scroll:原型是静态 HTML,需要同样执行 scroll + 截图。
1.D 差异分级
每页对比完成后,将差异分级:
| 级别 | 标准 | 示例 |
|---|---|---|
| 🔴 严重 | 布局结构不同、关键组件缺失、交互方式不同 | 原型有两列布局实际一列、缺少侧边栏、用标签页代替下拉菜单 |
| 🟡 中等 | 颜色/间距偏差、图标不一致、文字样式差异、非核心组件缺失 | 卡片圆角不一致、间距差 8px+、图标风格不同 |
| 🟢 轻微 | 微小像素差异、动画效果差异、可接受的响应式适配差异 | 1-2px 偏移、hover 效果不同、字体渲染差异 |
1.E 即时写入 Context(关键!)
每完成一个页面的 ui_diff_check 后,必须立即将结果写入 pipeline-context.md,防止上下文压缩导致结果丢失。
流程:
1. ui_diff_check 返回差异结果
2. 立即编辑 pipeline-context.md,在 "原型对比验证结果" 部分更新该页面的差异摘要
3. 格式:#### N/11 {页面名} — 严重程度 🔴/🟡/🟢/✅
4. 列出关键差异(精简为 3-7 条)
5. 继续下一个页面
为什么必须即时写入?
- 原型对比流程涉及 11 个页面的截图+对比,对话上下文可能在中途被压缩
- 如果不在每页完成后即时持久化,已完成的对比结果会丢失
pipeline-context.md是上下文压缩后的恢复锚点
context 中的记录格式示例:
## 原型对比验证结果 (Visual Diff)
### 全局问题(所有页面共通)
- 🔴 问题描述...
### 逐页结果
#### 1/11 Login — 严重 🔴
- 原型: pages/app/login.html → 实际: /login
- 🔴 差异1
- 🔴 差异2
- 🟡 差异3
#### 2/11 Register — 严重 🔴
- ...
差异记录格式(pipeline-state.yaml):
visual_diff_results:
dashboard:
prototype_page: "pages/app/dashboard.html"
actual_route: "/"
severity: moderate # critical / moderate / minor / match
viewport_compared: true
fullpage_compared: true
sections_compared: 3
differences:
- severity: critical
location: "首屏左侧"
description: "侧边栏缺失,原型有固定侧边栏导航"
section: 1
- severity: moderate
location: "首屏中部"
description: "上传区域卡片圆角:原型 16px,实际 8px"
section: 1
- severity: minor
location: "第2段"
description: "卡片间距略宽:原型 16px,实际 24px"
section: 2
Phase VD-2: 汇总报告
2.1 生成报告
生成 tests/e2e/screenshots/visual-diff/visual-diff-report.md:
# Visual Diff Report
生成时间: {timestamp}
对比页面数: {total_pages}
## Summary
| 级别 | 数量 |
|------|------|
| 🔴 严重 | {critical_count} |
| 🟡 中等 | {moderate_count} |
| 🟢 轻微 | {minor_count} |
| ✅ 匹配 | {match_count} |
## 严重差异(需修复)
### 🔴 {page_name}
- 原型截图: `visual-diff/{name}-prototype-viewport.png`
- 实际截图: `visual-diff/{name}-actual-viewport.png`
- 差异:
1. {具体差异描述}
2. {具体差异描述}
- 修复建议: {auto-fix 建议}
## 中等差异(建议修复)
### 🟡 {page_name}
...
## 轻微差异(可接受)
### 🟢 {page_name}
...
## 完全匹配
### ✅ {page_name}
2.2 更新 pipeline-state.yaml
visual_diff:
status: completed # completed / partial / failed
total_pages: 11
compared_pages: 10
skipped_pages: 1
critical_count: 0
moderate_count: 3
minor_count: 5
match_count: 2
report_path: "tests/e2e/screenshots/visual-diff/visual-diff-report.md"
timestamp: "2026-04-08T10:30:00Z"
2.3 输出结论
═══════════════════════════════════════════
Visual Diff Report 完成
═══════════════════════════════════════════
总页面: 10 对比, 1 跳过
🔴 严重: 0 → 无需修复
🟡 中等: 3 → 建议修复
🟢 轻微: 5 → 可接受
✅ 匹配: 2
报告: tests/e2e/screenshots/visual-diff/visual-diff-report.md
═══════════════════════════════════════════
2.4 修复决策
VD-2 报告完成后:
├── 🔴 严重差异 > 0 → 自动进入 Phase VD-3 全量修复
└── ✅ 无严重差异 → 流程结束
Phase VD-3: 全量自动修复
定位:在 Phase VD-2 发现严重差异后,自动修复所有视觉差异,使实际页面还原原型设计。以原型 HTML 为 Source of Truth,重写 UI 层。
核心原则:
- 先全局后逐页:全局修复(主题、导航、品牌)先做,避免逐页修复被全局问题干扰
- 保留后端逻辑:只改 UI 层(React 组件 + CSS),不动 API 路由和数据库
- 以原型为基准:直接从原型 HTML 提取设计 token 和组件结构
- 修复后必须验证:重新截图 + ui_diff_check 确认还原度
- 全程零确认、全自动
3.1 全局修复(先修复影响所有页面的共性问题)
从 pipeline-context.md 的"原型对比验证结果"中提取全局问题,按优先级修复:
全局问题按优先级:
P0: 主题系统(深色/浅色主题 — 影响 app 组页面)
P0: 品牌标识(图标 + 标题 — 影响所有页面)
P1: 管理后台导航结构(顶部水平导航 — 影响 admin 组页面)
P1: 通用组件样式(按钮 padding/圆角、输入框样式)
3.1.1 主题修复
1. 读取原型 HTML(如 pages/app/login.html),提取:
- 页面背景色(如 #000000)
- 卡片/容器背景色(如 #1E1E1E)
- 文字颜色(如 #FFFFFF)
- 主色调(如 #FF8400)
- 边框颜色(如 #333333)
2. 修改全局样式文件:
- globals.css / tailwind.config.ts
- 定义 CSS 变量或 Tailwind theme extend
3. 修改布局组件:
- 背景色 class 对齐
- 确保所有 app 组页面使用新主题
3.1.2 品牌标识修复
1. 从原型提取品牌元素:
- 图标(如 FontAwesome fa-bolt 闪电图标)
- 标题文字(如 "VividSpark")
- 副标题文字(如页面特有描述)
2. 定位品牌组件文件,替换:
- Logo 组件(图标 + 文字)
- 页面标题区域
3.1.3 管理后台导航修复
1. 从原型 admin 页面提取导航结构:
- 位置:顶部水平导航栏
- 菜单项:用户管理、内容审核、平台配置、数据分析、系统设置
- 右侧:管理员邮箱 + 登出按钮
2. 修改 admin 布局组件:
- 从左侧边栏 → 顶部水平导航
- 调整所有 admin 页面布局适配
3.1.4 全局修复完成后即时写入 context
编辑 pipeline-context.md,在"原型对比验证结果"下添加:
### 全局修复记录
- ✅/⚠️ 主题修复:{结果摘要}
- ✅/⚠️ 品牌标识修复:{结果摘要}
- ✅/⚠️ 导航结构修复:{结果摘要}
3.2 逐页修复循环
对 Phase VD-2 报告中每个有差异的页面,执行以下流程:
对每个有差异的页面(按严重程度排序,🔴 优先):
├── Step A: 解析原型 HTML
├── Step B: 定位并重写实际源码
├── Step C: 重新截图验证
└── 即时写入 context(每页完成后)
Step A: 解析原型 HTML
读取 prototype/pages/{group}/{name}.html,提取:
1. 设计 Token:
- 颜色:背景色、卡片色、文字色、主色调、边框色
- 字体:font-family、font-size、font-weight
- 间距:padding、margin、gap
- 圆角:border-radius
- 阴影:box-shadow
2. 组件列表:
- 表单字段(类型、标签、占位符、图标)
- 按钮(文字、颜色、大小、圆角)
- 卡片(布局、内容结构)
- 表格(列定义、操作按钮)
- 特殊组件(标签页、进度条、图表区域)
3. 文案:
- 页面标题、副标题
- 占位符文字
- 按钮文字
- 链接文字
4. 布局结构:
- grid/flex 布局
- 列数/行数
- 组件排列顺序
Step B: 定位并重写实际源码
1. 路由 → 源码文件映射:
/login → src/app/(auth)/login/page.tsx
/register → src/app/(auth)/register/page.tsx
/dashboard → src/app/(creator)/dashboard/page.tsx(或 src/app/page.tsx)
/publish → src/app/(creator)/publish/page.tsx
/pricing → src/app/(creator)/pricing/page.tsx
/settings → src/app/(creator)/settings/page.tsx
/admin/login → src/app/admin/login/page.tsx
/admin/users → src/app/admin/users/page.tsx
/admin/moderation → src/app/admin/moderation/page.tsx
/admin/analytics → src/app/admin/analytics/page.tsx
/admin/platforms → src/app/admin/platforms/page.tsx
/admin/settings → src/app/admin/settings/page.tsx
通用组件:
侧边栏 → src/components/layout/sidebar.tsx
头部导航 → src/components/layout/header.tsx(或 admin-header.tsx)
2. 以原型为 Source of Truth,对齐:
a. 组件结构:补全缺失组件(如 Remember me、FAQ、标签页、过滤器)
b. 布局结构:对齐网格列数、卡片 vs 列表等
c. 样式 class:Tailwind class 对齐原型视觉
d. 文案:品牌标题、占位符、按钮文字
e. 数据展示:如原型用图表,引入 chart 库并实现
3. 关键约束:
- 保留后端逻辑不变(API 调用、数据处理)
- 只修改 JSX 结构和 Tailwind class
- 新增的 UI 组件使用项目已有的设计系统组件
Step C: 重新截图验证
1. 重跑 Phase VD-1 的截图流程:
a. 截取修复后的实际页面截图
→ tests/e2e/screenshots/visual-diff/{name}-fixed-viewport.png
b. ui_diff_check 对比(修复后 vs 原型)
2. 结果判定:
- PASS(无 🔴/🟡)→ 标记 ✅ fixed
- FAIL → 调整修复策略,重试(最多 2 次)
- 仍 FAIL → 标记 ⚠️ needs-manual-fix,记录原因
3. 每页修复完成后即时写入 pipeline-context.md:
#### N/11 {页面名} — ✅ fixed / ⚠️ needs-manual-fix
- 修复内容: {具体修复了什么}
- 验证结果: {ui_diff_check 摘要}
3.3 修复后二次对比(全量验证)
全局 + 逐页修复完成后:
1. 对所有 11 页重跑 Phase VD-1 截图 + ui_diff_check
2. 生成修复前后对比:
- 修复前: visual-diff/{name}-actual-viewport.png
- 修复后: visual-diff/{name}-fixed-viewport.png
3. 更新 visual-diff-report.md(添加修复结果列)
4. 更新 pipeline-state.yaml visual_diff 部分
3.4 最终报告
visual_diff:
status: completed # completed / partial / failed
total_pages: 11
compared_pages: 11
fixed_pages: 8 # 新增
manual_fix_needed: 3 # 新增
critical_count: 0 # 修复后的数值
moderate_count: 1
minor_count: 5
match_count: 5
report_path: "tests/e2e/screenshots/visual-diff/visual-diff-report.md"
fix_report_path: "tests/e2e/screenshots/visual-diff/visual-diff-fix-report.md"
timestamp: "2026-04-08T12:00:00Z"
输出结论:
═══════════════════════════════════════════════
Visual Diff Report + 自动修复 完成
═══════════════════════════════════════════════
总页面: 11 对比, 0 跳过
✅ 已修复: 8
⚠️ 需手动修复: 3(见报告)
修复前: 🔴 11 🟡 0 🟢 0 ✅ 0
修复后: 🔴 0 🟡 1 🟢 5 ✅ 5
报告: tests/e2e/screenshots/visual-diff/visual-diff-report.md
修复报告: tests/e2e/screenshots/visual-diff/visual-diff-fix-report.md
═══════════════════════════════════════════════
视口配置
| 目标 | 宽度 | 高度 | 用途 |
|---|---|---|---|
| 桌面端(管理后台) | 1440 | 900 | admin 组页面 |
| 桌面端(前端) | 1440 | 900 | app 组页面(如有移动端原型则用移动视口) |
| 移动端 | 375 | 812 | 移动端原型(如有 mobile 目录) |
错误处理
| 错误场景 | 处理方式 |
|---|---|
| 原型服务启动失败 | 检查端口占用,尝试其他端口,报告错误 |
| 实际页面无法访问 | 标记该页面为 error,继续下一个 |
| 实际页面需要认证且登录失败 | 标记该页面为 error,记录认证错误 |
| ui_diff_check 超时 | 降级为仅 analyze_image 语义分析 |
| 截图文件过大 | fullPage 截图限制最大 8000px 高度 |
| 原型页面无对应实际路由 | 标记为 skipped,记录在报告中 |
注意事项
- 视口必须一致:原型和实际页面使用完全相同的视口尺寸截图
- 数据状态差异:原型是静态设计,实际页面有真实数据。对比时忽略因数据内容不同导致的差异(如文字内容、图片内容),只关注布局和组件差异
- 认证页面:login/register 页面不需要登录即可访问,直接截图对比
- 原型静态特性:原型没有动画和过渡效果,实际页面可能有,这些差异标记为 🟢 轻微
- 自动修复:Phase VD-2 发现严重差异后自动进入 Phase VD-3 全量修复,全程零确认
- 保留后端逻辑:修复只改 UI 层(React 组件 + CSS),不动 API 路由和数据库
- 重试上限:每页最多修复重试 2 次,仍失败则标记为需手动修复