# 设计稿像素验收 — 详细技术方案

> 适用范围：微信小程序设计稿还原的截图、对比、门禁、分析与二次还原闭环。  
> 演进记录：`docs/pixel-gate/scheme-log.md`（改门禁算法时先读再补）。  
> 样例 Flow：`{flowsRoot}/Flow-234/ui_review/`。

---

## 1. 目标与原则

### 1.1 目标

在**不手写 ROI / 不依赖 `elements[]` 枚举**的前提下，对「设计稿 Group」与「实际页面裁剪图」做可重复的像素级验收，卡住：

- 文字 / 图标绝对位置偏差（Δx / Δy）
- 同行元素内部间距（如保存按钮图标↔文案）
- 行内基线不一致（如「实付」不动、金额单独下移）
- 容器内边距（按钮左右 padding）
- 相对分隔线 / 模块底边的上下 padding

### 1.2 设计原则

| 原则 | 说明 |
|------|------|
| 模块优先 | 按页面一级代码模块裁剪对比；组间距单独卡门禁，不全页绝对坐标硬比 |
| 自动提取 | 墨迹 token 从像素自动切出可见元素，不写 selector |
| 双层门禁 | 绝对位移 + 相对关系；百分比只作粗筛 |
| 噪声降权 | AA / 透明叠底 / 微位移降权，避免假失败 |
| 可闭环 | 失败可生成修复提示词，可选 AI 多轮还原 |

### 1.3 非目标

- 不做业务功能自动化测试（需求用例见 `docs/flow-cases/technical-design.md`，不走本像素门禁）
- 不以 `elements[]` 白名单为主方案（已退出，见 scheme-log §3）
- 不保证字体渲染引擎差异为 0（允许 1–2px 基线容差）

---

## 2. 系统架构

```
┌─────────────────┐   ┌──────────────────┐   ┌─────────────────┐
│ screenshot.config│→ │ auto-screenshot  │→ │ page.png / raw  │
│ (+ Figma 可选)   │   │ + inject-mock    │   │ (+ element-layout)│
└─────────────────┘   └──────────────────┘   └────────┬────────┘
                                                      │
                      ┌──────────────────┐            │
                      │ locate-module    │←───────────┘
                      │ MAD 模板匹配裁剪 │
                      └────────┬─────────┘
                               │ crop.png + design.png
                      ┌────────▼─────────┐
                      │ pixel-diff       │
                      │ pixelmatch       │
                      │ + weight/cluster │
                      │ + row-align      │
                      │ + ink-tokens ★   │
                      │ → evaluatePass   │
                      └────────┬─────────┘
                               │ diff-report.json / diff.png
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
       diff-analyze      ai-bridge         acceptance-doc
       提示词/bundle     Cursor Agent      飞书「需求UI验收」
              └────────────────┬────────────────┘
                               ▼
                          re-restore
                      （截图→对比→修复循环）
```

### 2.1 npm 命令

| 命令 | 入口 | 职责 |
|------|------|------|
| `npm run screenshot` | `scripts/screenshot/auto-screenshot.js` | 开发者工具截图 |
| `npm run pixel-diff` | `scripts/compare/pixel-diff.js` | 对比 + 门禁 |
| `npm run diff-analyze` | `scripts/analyze/diff-analyze.js` | 失败项生成分析/修复提示词 |
| `npm run re-restore` | `scripts/analyze/re-restore.js` | 多轮闭环还原 |
| `npm run acceptance-doc` | `scripts/analyze/acceptance-doc.js` | 通过后写飞书验收文档 |
| `npm run test:crop` | `scripts/__tests__/crop-module.test.js` | 模块裁剪单测 |

### 2.2 常用调用

```bash
npm run screenshot -- 234 --module group-1
npm run pixel-diff -- 234 --module group-1
npm run diff-analyze -- 234
npm run re-restore -- 234 --module group-1 --maxRounds=3 --dry-run
npm run acceptance-doc -- 234
```

- `--flowId` / `-f`：缺省当天日期；数字 `234` → `Flow-234`
- `--module` / `--modules`：逗号分隔模块 id
- `--prd-url`：`acceptance-doc` 写入 `docs.js` 的 `PrdUrl`
- 小程序工程：须在项目根目录执行验收；用当前项目绝对路径（`WECHAT_PROJECT_PATH` 或 cwd 向上找 `project.config.json`）
- `flowsRoot`：首次 TTY 三选一（用户目录 / 项目同级 / 自定义），写入 `~/.lucp-flows.json`；`LUCP_FLOWS_ROOT` 可覆盖

---

## 3. 端到端流水线

### 3.1 配置准备

`ensureFlowScreenshotConfig(flowId)`：

1. `resolveFlowsRoot()` 后确保 `{flowsRoot}/Flow-{id}/ui_review/`
2. 无配置或 `pages` 为空时确认页面路径（不输模块个数）：整页一张 `group-1`（Figma 父节点整帧或 `{pageDir}/design-full.png` 拷为 `group-1/design.png`），并写 `capture.mode='fullpage'`。截图后按页面一级代码模块拆块；`--groups N` 才手工拆。不按 Figma 子 Group 拆。
3. 已有模块缺稿时提醒复制 `group-*/design.png`；回车后重读配置，已删除的模块不再要求 design.png
4. 可选：`figma-design.js` 按模块 `figmaLinkNode` 拉 2x `design.png`

### 3.2 截图

1. `resolveDevtoolsHttpPort()` 解析服务端口
2. `automator.launch` 带 `--compile-condition` 指向 `entry || path`，避免停在上次编译页
3. `injectRequestMock` 稳定接口数据
4. `reLaunch(entry)`（有 `steps` 时禁止 `reLaunch` 到 `path`，避免丢页面栈/来源记录）。被开发者工具拉到验收页则拉回 `entry`。再按 `steps` tap / `waitFor` 点进去
5. 按 `pages[].capture.mode` 截图（缺省 `viewport` 拍一屏；`fullpage` 滚动拼长图）→ `enhanceScreenshot`（保留 `.raw.png`，增强版写 `page.png`）
6. `code-modules.js` 按页面 `usingComponents` + 主滚动容器一级原生块取运行时矩形（过滤导航/胶囊/过小块），裁 `{id}/actual.png`；反向 MAD 从 `design-full.png` 切 `{id}/design.png`。≥2 块且配置仍是单 `group-1` 时展开 modules。失败回退整页。
7. 有 `prepare` 的模块：在目标页 `setData` / 滚动 / `tap` 后单独拍 `actual.png`
8. 若配置 `elements[]`：采集 `element-layout.json`（非主路径）

### 3.3 像素对比

对每个 page × module：

1. 有代码模块裁块时用 `{id}/actual.png` 与反向 MAD 的 `{id}/design.png` 做块内对比；否则 `resolveDesignPath` / `resolveActualPath` + `cropModuleFromPage`
2. 写 `compare.png`
3. sharp 对齐尺寸 → `pixelmatch` + ink / 行带（块内坐标系，避免上错带偏下面）
4. ≥2 块再比相邻运行时间隙 vs 设计 `designRegion`
5. `evaluatePass` → `diff-report.json`

### 3.4 分析与闭环（可选）

- `diff-analyze`：仅处理 `status=compared && passed=false`
- `re-restore`：循环截图→对比→AI 修；`accepted` 模块默认跳过（`--force` 重验）

### 3.5 通过后飞书文档（可选）

本次对比模块全部通过后，由 Agent 询问是否生成验收文档。确认后：

1. 读 `{flowsRoot}/Flow-{id}/docs.js` 的 `PrdUrl`；没有则询问需求文档链接并写入
2. `npm run acceptance-doc -- <flowId>` 调用飞书 CLI
3. 在我的文档库（`my_library`）节点「需求UI验收」下创建 `{flowId} UI验收`，或按 `UIReviewUrl` 二次更新（验收结果表追加行）。Wiki 节点 token 缓存在 `~/.lucp-flows.json`。
4. 未安装 `lark-cli` 时退出码 2，按官方安装指南由 Agent 协助安装后再跑

---

## 4. 产物目录约定

根：`{flowsRoot}/Flow-{id}/`  
`flowsRoot` 为选中父目录下的 `lucp-flows` 文件夹。

```
{flowsRoot}/Flow-xxx/
├── docs.js                              # PrdUrl / UIReviewUrl
└── ui_review/
```

`{flowsRoot}/Flow-xxx/ui_review/`：

```
{flowsRoot}/Flow-xxx/ui_review/
├── screenshot.config.js
├── diff-report.json
├── acceptance.json
├── acceptance-{pageName}.md
├── fix-agent-prompt.md
├── re-restore-{pageName}.json
└── {pageName}/                          # /pages/a/b → pages_a_b
    ├── page.png / page.raw.png
    ├── design-full.png                  # 新建时整页 2x 源稿，同时拷为 group-1/design.png
    ├── code-modules.json                # 截图后按代码模块发现的矩形
    ├── capture-meta.json                # sticky / statusBarHeight / 窗口尺寸
    ├── _scroll/                         # fullpage 中间帧（对比不读）
    └── {moduleId}/                      # 如 product-header / group-1
        ├── design.png
        ├── crop.png
        ├── compare.png                  # 上：双叠图；下：左+下还原稿
        ├── diff.png
        ├── actual.png / actual.raw.png  # prepare 模块
        ├── element-layout.json          # 可选
        ├── analysis-prompt.md
        ├── fix-agent-prompt.md
        ├── analysis-bundle.json
        └── analysis-result.json         # 可选
```

**实际页面截图源优先级**（`resolveActualPath`）：

`actual.raw.png` > `actual.png` > `page.raw.png` > `page.png` > 遗留扁平命名

---

## 5. Flow / Module 配置

### 5.1 `screenshot.config.js`

```javascript
module.exports = {
  waitMs: 1500,
  targetWidth: 750,
  sharpen: true,
  pages: [
    {
      path: '/pages/order-detail/order-detail', // 验收目标页，须在 app.json 注册
      // figmaLinkNode: 'https://www.figma.com/design/...?node-id=1-140', // 父节点整帧，新建默认不拆子 Group
      // entry: '/pages/home/home',  // 必须连跳时的起点，可带 query
      // steps: [                    // A 点 a → B 点 b → C
      //   { tap: '.btn-a', waitFor: '/pages/list/list' },
      //   { tap: '.btn-b', waitFor: '/pages/order-detail/order-detail' },
      // ],
      // capture: { mode: 'fullpage', scroll: 'auto', overlap: 80, stickyTop: 'auto', stickyBottom: 'auto', maxHeight: 8000 },
      modules: [
        {
          id: 'group-1',
          // name: '顶栏',
          // design: 'design.png',
          // figmaLinkNode: 'https://www.figma.com/design/...?node-id=1-140',
          // prepare: { setData: {...}, scrollTop: 600, scrollIntoView: '.sel', tap: '.sel', waitMs: 800 },
          // elements: [],           // 已非主方案
          // elementRelations: [],
        },
      ],
    },
  ],
};
```

### 5.2 设计稿来源优先级

1. 模块目录已有 `design.png`
2. `mod.design` 指定文件名
3. `figmaLinkNode` → Figma API 2x PNG（`FIGMA_FORCE=1` 强制刷新）
4. 扫描 `Group-*.png` / 遗留 `Group-{pageName}.png`
5. 新建无 Figma 时：`{pageDir}/design-full.png` 拷为 `group-1/design.png`（不按空白带切开）

### 5.3 `docs.js`

```javascript
module.exports = {
  PrdUrl: 'https://...',   // 需求文档
  UIReviewUrl: '',         // 飞书 UI 验收文档，首次创建后回写
};
```

### 5.4 `acceptance.json`

```json
{
  "pages_order-detail_order-detail": {
    "group-1": {
      "status": "pending|accepted",
      "at": "ISO8601",
      "diff": 1.42,
      "round": 1,
      "failReasons": []
    }
  }
}
```

---

## 6. 核心模块说明

### 6.1 截图：`auto-screenshot.js`

| 函数 | 职责 |
|------|------|
| `resolveDevtoolsHttpPort` | 环境变量 / `.ide` 读 HTTP 端口 |
| `enhanceScreenshot` | 原始落 raw；超采样锐化到 `targetWidth` |
| `preparePageState` | 目标页上 `setData` / 滚动 / `tap` / 等待 |
| `arriveAtReviewPage` | `reLaunch(entry)` → `steps` tap/`waitFor` → 确认 `path` |
| `collectElementLayout` | automator 节点几何（可选） |
| `captureScreenshots` | 主流程；`capture.mode='fullpage'` 走 `scroll-capture.js` |

默认：`cliPath=/Applications/wechatwebdevtools.app/Contents/MacOS/cli`，`waitMs=1500`，`targetWidth=750`。全新页写入 `capture.mode='fullpage'`。

### 6.1.1 滚动截屏：`scroll-capture.js`

零业务改动。默认 `viewport` 一屏。可选：

- **档 1** `prepare.scrollTop` / `scrollIntoView` / `scroll.to`：滚到模块后拍 `actual.png`
- **档 2** `capture.mode='fullpage'`：探测 page 或最大 `scroll-view`，重叠截屏拼 `page.png`；中间帧在 `{pageDir}/_scroll/`

`scroll: 'auto'` 先试 `pageScrollTo`，不动再找 `scroll-view`。都滚不动则失败（受控 `scroll-top` / 自绘滚动不改业务兜底）。先小滚两帧探测 sticky 顶/底（默认 `auto`，上限各 25% 视口），再按 `viewH - stickyTop - stickyBottom - overlap` 步进；**不以** `scrollHeight - viewH` 封顶（automator 的 `scrollHeight` 常不含 `padding-bottom`），`scrollTop` 不再增加才停，末尾再过冲一屏。中间帧裁掉重复固定栏再拼接；显式 `stickyTop` / `stickyBottom` 数字仍覆盖。`maxHeight=8000` 封顶。探测结果写入 `{pageDir}/capture-meta.json`。半透明导航叠在滚动内容上时可能探测为 0，可手写高度。

### 6.2 模块定位：`locate-module.js`

| 步骤 | 参数默认 |
|------|----------|
| 粗搜 | `searchDownscale=4`，`searchStep=2`，`xSearchRadius=32` |
| 精搜 | `refineDownscale=2`，`refineRadius=24` |
| 分数 | MAD `maxScore=0.35` |
| 顶对齐 | 白顶扫描前 120px，25% 白像素阈值 |
| 模块判定 | `designH/pageH < heightRatioThreshold(0.55)` |

代码模块 ≥2 时先局部再结构：每块用已裁的 `actual.png` 与反向 MAD 得到的 `design.png` 在块内坐标系对比（`auto`，高度接近则 `full`）。整页不再跑绝对坐标 ink。反向 MAD 失败该块 `missing-design-match`，其它块继续。

0 或 1 块走 `auto`：稿高接近或超过整页则 `full`（按设计宽度、取两边较大高度，短边底部垫白，不再压成 750×1624）。

≥2 块用运行时矩形比相邻间隙，设计间隙来自反向 MAD 的 `designRegion`（或 `designGapAfter` / 在 `design-full.png` 上定位）。`|Δ| ≤ maxGroupGapDelta(2)`，失败写入后者 `failReasons`。固定底栏只作为最后一块。导航/胶囊不进模块。

对比忽略状态栏：`ignoreTop` 默认 `statusBarHeight * compareWidth/windowWidth`（约一条状态栏+胶囊），该带内不计入百分比、不报 ink。模块裁块若 `rect.y` 已离开页顶则不再忽略。不豁免自定义返回按钮整条导航。

### 6.3 差异加权：`diff-weight.js`

| 类别 | 权重 | 含义 |
|------|------|------|
| `transparentOverlap` | 0 | 设计透明被底色填充 |
| `backgroundBleed` | 0.15 | 灰底互渗 |
| `renderHalo` | 0.25 | AA / 边缘光晕 |
| `microShift` | 0.3 | 邻居匹配微位移（tol≤10） |
| `structureShift` | 1 | 结构位移（Y≥2 或 X≥3） |
| `real` | 1 | 真实差异 |

### 6.4 差异聚类：`diff-cluster.js`

- 腐蚀孤立描边点 → 按面积聚类（`minRegionArea=100`，自适应下限 `area/800`）
- `likelyCause`：`spacing` / `layout` / `text-overflow` / `visual` 等（分析提示，非硬门禁）

### 6.5 行带对齐：`row-align.js`

- 暗色行带（RGB<160）整宽聚簇后比 `|Δy|`
- 门禁：`maxRowDelta=3`，`maxMedianRowDelta=2`
- 局限：左右平均，细粒度交给 ink 相对关系

### 6.6 元素白名单：`element-geometry.js`（辅 / 已弱化）

无 `elements[]` 时直接 `passed: true`。保留供特殊页强制卡某个 selector。

---

## 7. 主门禁：墨迹 token（`ink-tokens.js`）

### 7.1 提取

```
像素 → ink 掩膜（深色 ∪ 饱和色）
     → 连通域
     → container（大而空的描边框）vs ink part
     → 行聚类 → gap 合并 → 空白列再切
     → tokens[]
```

关键阈值：

| 项 | 默认 |
|----|------|
| darkThreshold | 195 |
| tokenGap（行内合并） | 4 |
| minTokenPixels | 8 |
| minCriticalPixels | 18 |
| minMissingPixels | 40 |
| minShapeIou | 0.12 |
| minMatchWidthRatio | 0.75 |
| matchRadiusX / Y | 48 / 24 |

### 7.2 绝对位移

critical：`pixels ≥ 18` 且 `shapeIou ≥ 0.12` 且 `min(w)/max(w) ≥ 0.75`

| 规则 | 阈值 |
|------|------|
| \|Δx\| | ≤ 2；**宽≤12 且高≤20 的小箭头不卡 Δx** |
| \|Δy\| | ≤ 2；**高度差 >6px 的配对不卡 Δy** |
| missing | ≥40px 未匹配；24px 内高度相近墨块可覆盖 |

### 7.3 相对关系（配对后）

| 类型 | 规则 | 阈值 |
|------|------|------|
| `gap` | 紧邻组内 `next.x - prev.right`；两边宽度差 ≤4；设计 gap ≤40；仅 comparable；跳过小箭头 | \|Δ\| ≤ 2 |
| `row-y` | 紧邻组内 Δy 相对中位（space-between 左右分开）；仅 comparable 配对 | \|outlier\| ≤ 1 |
| `inset` | container 内 ink 四边 inset | \|Δ\| ≤ 2 |
| `pad` | 浅色横线→下一行；末行→模块底（用全部 ink，含粘连块） | \|Δ\| ≤ 1 |

浅色横线单独扫（不进 ink 掩膜）：近灰 `#eee` 量级，行覆盖≥10%、span≥40%，且不被暗文字行污染。

### 7.4 失败文案示例

```
ink token@(598,283,13x21): gap +3px (design 13 actual 16)
ink token@(535,398,20x22): row-y -1.5px (...)
ink token@(561,274,114x38): inset-bottom +3px (...)
ink token@(0,362,1x34): pad-top +3px (...)
```

修复提示词会标注：改相邻间距/padding，不要只挪单个绝对坐标。

---

## 8. 综合通过条件：`evaluatePass`

### 8.1 必须全部满足

| 门禁 | 条件 |
|------|------|
| 加权差异 | `diffPercentage < 3.0%` |
| 原始差异 | `rawDiffPercentage < 4.0%` |
| 行带 | `max\|Δy\| ≤ 3` 且 `median\|Δy\| ≤ 2` |
| ink token | `enabled` 且 `status=compared` 时 `inkTokens.passed` |
| 组间距 | 一页 ≥2 模块且能解析设计间隙时，相邻 `matchRegion` `|Δ| ≤ 2` |
| element / runtime | 无配置则跳过 |

### 8.2 ink 启用时不再一票否决

| 辅判 | 原阈值 | 现状 |
|------|--------|------|
| `shiftRatio` | >0.85（且 raw≥2.5%） | ink 活跃时关闭 |
| `structureShiftPct` | >1.2% | 同上 |
| `shiftPct` | >3.0% | 同上 |

原因：AA / 描边邻居匹配会把「视觉已过」撑成位移占比假失败。

### 8.3 通过语义

```
passed = 像素粗筛过
       ∧ 行带辅判过
       ∧ ink 绝对位移过
       ∧ ink 相对关系过
```

---

## 9. 微信开发者工具对接

### 9.1 服务端口

开发者工具：**设置 → 安全设置 → 开启服务端口**。

注意：界面显示的端口号可能过期；本机真实端口以：

`~/Library/Application Support/微信开发者工具/<hash>/Default/.ide`

为准，`.ide-status` 为 `On` 时优先。

### 9.2 脚本解析顺序

1. `WECHAT_DEVTOOLS_PORT` 环境变量
2. 扫描上述 `.ide` / `.ide-status`（On + 最新 mtime）
3. 通过 CLI：`args: ['--port', String(httpPort)]`

> `miniprogram-automator` 的 `port` 选项是 **WebSocket auto-port**，不是 HTTP 服务端口；HTTP 口必须走 CLI `--port`。

### 9.3 常见故障

| 现象 | 处理 |
|------|------|
| `http port is open` | 开服务端口并重启开发者工具 |
| 界面端口与 `.ide` 不一致 | 以 `.ide` / `lsof` 为准，或设 `WECHAT_DEVTOOLS_PORT` |
| 截图卡住 | 关闭残留 automator 会话；确认项目已打开 |

---

## 10. AI 分析与二次还原

### 10.1 `diff-analyze` / `prompt-templates/diff-fix.js`

输入：`diff-report` 失败项 + 页面/组件 WXML·WXSS + 图路径。  
输出：`analysis-prompt.md`、`fix-agent-prompt.md`、`analysis-bundle.json`。  
提示词会展开 ink 的 `matches` / `missing` / `relations`。

### 10.2 `ai-bridge.js`

- 有 `CURSOR_API_KEY`：调 `@cursor/sdk` Agent 分析/应用修复
- 否则：只落盘提示词（prompt-only）

### 10.3 `re-restore.js`

循环：截图 → `comparePageModule` → 分析 → 修复。  
终止：`passed` / `maxRounds`（默认 3）/ dry-run / 缺 Key。  
状态写入 `acceptance.json`。

**不要死循环改间距：** 差异若要靠代码条件判断才能兼顾多状态（有/无括号、全角半角等），且单一 margin 顾不了，先出方案等人确认，不要反复截图重试。提示词见 `prompt-templates/diff-fix.js` 的 `CONDITIONAL_FIX_REMINDER`。

---

## 11. 全局默认阈值一览

来源：`scripts/compare/pixel-diff.config.js`

| 类别 | 键 | 默认 |
|------|----|------|
| pixelmatch | threshold | 0.3 |
| 通过 | passPercentage | 3.0 |
| 通过 | rawPassPercentage | 4.0 |
| 行带 | maxRowDelta | 3 |
| 行带 | maxMedianRowDelta | 2 |
| ink | maxDeltaX/Y | 2 / 2 |
| ink | minMatchWidthRatio | 0.75 |
| ink | maxGapDelta | 2 |
| ink | maxLocalGap | 40 |
| ink | maxIntraRowDeltaY | 1 |
| ink | maxInsetDelta | 2 |
| ink | maxEdgePadDelta | 1 |
| 定位 | maxScore | 0.35 |
| 定位 | heightRatioThreshold | 0.55 |
| 组间距 | maxGroupGapDelta | 2 |
| 状态栏 | ignoreTop | null（`statusBarHeight * 画布/窗口宽`；模块 `rect.y>0` 时不再忽略） |

---

## 12. 演进摘要

| 阶段 | 方案 | 结论 |
|------|------|------|
| 1 | 全局百分比 | 粗筛保留；对间距不敏感 |
| 2 | 行带对齐 | 辅判保留；median 提到 2 |
| 3 | `elements[]` | **退出主方案** |
| 4 | ink 绝对位移 | 保留 |
| 5 | ink 相对关系 | 主门禁；shift 辅判在 ink 活跃时关闭 |
| 6 | 宽度比过滤粘连 | 宽度比 <0.75 不当位移 |
| 7 | 小箭头豁免 Δx | 宽≤12 高≤20 不卡绝对 x / 邻 gap |

细节与每次改造动机见 `scheme-log.md`。

---

## 13. 已知局限

1. MAD 匹配 score>0.35 时裁剪失败；Group 大于视口截图时裁剪失败（`capture.mode='fullpage'` 可解）  
2. 聚类可能把整模块合成单区（分析提示会提示）  
3. 截图锐化/超采样会轻微改变像素统计  
4. 字形粘连仍可能产生伪 missing；已用邻近覆盖缓解，极端字距仍需人工看图  
5. `re-restore` 依赖 Cursor Key；dry-run 只产提示词  
6. Figma 拉取需 token  
7. prepare 态 reset 较简，复杂弹层需人工保证可复现
8. 无滚动容器 / 受控 `scroll-top` / 自绘滚动时 fullpage 失败  
9. 虚拟列表滚过去才渲染，拼图可能缺尚未加载的块；代码模块发现同样看不到未挂载节点
10. 还原极差时反向 MAD 可能对不上（单块 `missing-design-match`，不级联）
11. 半透明导航叠在滚动内容上时 sticky 探测可能为 0，需手写 `stickyTop`
12. automator `scrollHeight` 可能不含 `padding-bottom`；已改为滚到 `scrollTop` 不再增加，不按 `scrollHeight - viewH` 封顶

---

## 14. 关键文件清单

### 核心脚本

- `scripts/screenshot/auto-screenshot.js`
- `scripts/screenshot/scroll-capture.js`
- `scripts/screenshot/code-modules.js`
- `scripts/screenshot/figma-groups.js` / `split-design.js`
- `scripts/shared/flows-root.js` / `flow-docs.js` / `repo-root.js` / `project-root.js` / `page-modules.js`
- `scripts/screenshot/screenshot.config.js`
- `scripts/compare/pixel-diff.js` / `pixel-diff.config.js`
- `scripts/compare/ink-tokens.js`
- `scripts/compare/row-align.js`
- `scripts/compare/element-geometry.js`
- `scripts/compare/diff-weight.js` / `diff-cluster.js`
- `scripts/compare/locate-module.js`
- `scripts/analyze/diff-analyze.js` / `diff-analyze.config.js`
- `scripts/analyze/ai-bridge.js`
- `scripts/analyze/re-restore.js` / `re-restore.config.js`
- `scripts/analyze/acceptance.js` / `acceptance-doc.js`
- `scripts/analyze/prompt-templates/diff-fix.js`
- `scripts/screenshot/figma-design.js`
- `scripts/screenshot/inject-mock.js`

### 文档与样例

- `docs/pixel-gate/scheme-log.md`
- `docs/pixel-gate/technical-design.md`（本文）
- `{flowsRoot}/Flow-234/docs.js`
- `{flowsRoot}/Flow-234/ui_review/screenshot.config.js`
- `{flowsRoot}/Flow-234/ui_review/acceptance.json`
- `{flowsRoot}/Flow-234/ui_review/diff-report.json`

### 测试

- `scripts/__tests__/crop-module.test.js`

---

## 15. 推荐使用路径

**日常验收一个模块：**

```bash
# 1. 开发者工具已开「服务端口」；在小程序项目根目录取绝对路径
WECHAT_PROJECT_PATH=<当前项目绝对路径> npm run screenshot -- 234 --module group-1
npm run pixel-diff -- 234 --module group-1
# 2. 看控制台 passed= / fail=[...]
# 3. 未过则按 ink failReasons 改 WXSS，再重复 1–2
#    若要条件判断、单一间距顾不了：先出方案确认，不要重试改 margin
#    折下 / Group 超一屏：配 capture.mode='fullpage' 或 prepare.scrollTop / scrollIntoView
```

**需要 AI 辅助修复：**

```bash
npm run diff-analyze -- 234
# 或
npm run re-restore -- 234 --module group-1 --dry-run
```

**门禁通过后写飞书验收文档：**

```bash
npm run acceptance-doc -- 234
```

**改门禁算法时：**

1. 先读 `scheme-log.md`
2. 改 `ink-tokens.js` / `pixel-diff*`
3. 用 Flow-234 回归：`screenshot` + `pixel-diff`
4. 在 `scheme-log.md` 追加一条：问题 + 思路 + 阈值变化
