# 04 · 验证结果
> 验证环境:Windows · Chromium 154(Playwright 1.62 内置 chromium-1234)· 视口 1440×900 · deviceScaleFactor 1
> 复跑命令:`powershell -File verify-headless.ps1`(等价于 `node _verify/run.mjs`)
> 产物:`_verify/f*.png` 逐帧截图 + 控制台时序表
---
## 0. 验证方法与其诚实边界
### 方法
真实 DSH 桌面窗口**无法在无人干预下自动重启并录像**(重启即关闭本会话)。因此采用**结构等价验证**:
1. 造一个 `_verify/harness.html`,复刻桌面版前端拿到注入后的真实执行路径:
- 加载与 `dist/index.html` 一致的空壳(只有 `
`)
- 复刻 `serveWebDocument` 注入的 `__DSH_BOOT_READY__` 行
- 复刻前端 `hM(e, n)` 注入行解释器(`global` / `script` / `style` / `html` 四种 case 全部照抄)
- 复刻前端自带加载页 `tM` 的关键特征:`#root` 下有一个 `data-dsh-boot` 元素
- 复刻 AppFrame 结构:三列 grid(`280px / minmax(400px,1fr) / 340px`)、`[data-shell-overlay]`、三个 `[data-slot]` 锚点且**均为 `display:contents`**
2. **注入的是未经改写的真实 `lib/boot-anim.css` 与 `lib/boot-anim.js`**(`run.mjs` 直接读取文件内容拼进 `__INJECTIONS__`)。这一点很重要:验证的不是「一个类似实现」,而是**即将上线的那个文件本身**。
3. 真实时间采样:从 `goto` 起按目标时刻截图,同时读取 `getComputedStyle`。
### 诚实的边界
| 未验证项 | 原因 |
|---|---|
| 真实 DSH 窗口中的最终观感 | 需重启桌面版;重启会终止当前会话 |
| 真实桌面版的 IPC 往返时机 | 同上 |
| 注入晚于 React 首次挂载 | 理论推断:舞台 `z-index` 极高 + `waitApp` 会等 UI 就绪,最差是 UI 闪一帧后被覆盖 |
**上表三项在本文中一律标注为「未验证」,不从等价验证的结果里外推。**
---
## 1. 分镜 ①~⑦ 时序实测
采样点为「goto 之后的毫秒数」,注入发生在 `elapsed ≈ 200ms`。
| 采样 t | 实测 elapsed | 舞台 | 鲸鱼 opacity | 鲸鱼 transform | 应用挂载 | 判定 |
|---|---|---|---|---|---|---|
| 0 | 201 | 在 | 0.00 | `scale(0.86)` | 否 | ① 黑屏 ✓ |
| 350 | 551 | 在 | 0.96 | `scale(0.994)` | 否 | ② 鲸鱼淡入中 ✓ |
| 650 | 862 | 在 | **1.00** | `scale(1.000)` | 否 | ② 完成,鲸鱼完整 ✓ |
| 950 | 1159 | 在 | **0.00** | `scale(1.04)` | 否 | ③ 鲸鱼让位,双线接棒 ✓ |
| 1100 | 1307 | 在 | 0.00 | `scale(1.04)` | 否 | ③ 线延伸中 ✓ |
| 1300 | 1505 | 在 | 0.00 | `scale(1.04)` | 否 | ③→④ 交界 ✓ |
| 1600 | 1802 | 在 | 0.84 | `scale(0.987)` | **是** | ④ 线构筑成鲸鱼 ✓ |
| 1900 | 2108 | 在 | **1.00** | `scale(1.000)` | 是 | ④ 完成 ✓ |
| 2300 | 2507 | 在 | 1.00 | `scale(1.000)` | 是 | ④ 定格,等 UI 就绪 ✓ |
| 2700 | 2908 | 在 | 1.00 | `scale(1.000)` | 是 | 同上(闸门 hold)✓ |
| 3000 | 3207 | 在 | 1.00 | `scale(1.000)` | 是 | 同上(闸门 hold)✓ |
| 3300 | 3506 | 在 | 0.99 | `scale(1.000)` | 是 | ⑤⑥ 揭幕开始 ✓ |
| 3600 | 3804 | 在 | 0.70 | `scale(1.000)` | 是 | ⑥ 鲸鱼缓缓消失 ✓ |
| 3900 | 4103 | 在 | **0.04** | `scale(1.000)` | 是 | ⑥ 完成 ✓ |
| **4300** | 4513 | **已移除** | — | — | **是** | 舞台自毁,露出真实 UI ✓ |
| 4700 | 4905 | 已移除 | — | — | 是 | ⑦ 入场中 ✓ |
| 5200 | 5405 | 已移除 | — | — | 是 | ⑦ 入场完成 ✓ |
| 6000 | 6201 | 已移除 | — | — | 是 | 稳定态 ✓ |
**读法**:②③④ 依次独占画面,④ 收笔后停在鲸鱼完整态(2300–3000ms)**等待 UI 就绪**,拿到就绪信号才进入 ⑤⑥。这正是设计意图。
### 每个分镜的落点
| 分镜 | 实测时间窗 | 结果 |
|---|---|---|
| ① 黑屏 | 0 – ~500ms | ✅ 全黑 |
| ② 屏幕中央出现鲸鱼 | ~200 – ~900ms | ✅ 鲸鱼居中淡入至完整 |
| ③ 转全黑、两线向中间延伸 | ~950 – ~1400ms | ✅ 鲸鱼退,双线接棒 |
| ④ 两线构筑成鲸鱼并勾轮廓 | ~1400 – ~2150ms | ✅ 压扁线展开为鲸鱼 + 描边收笔 |
| (闸门 hold) | ~2150 – ~3300ms | ✅ 等 UI 就绪 |
| ⑤ 中央横线上下展开,一分为二 | ~3300 – ~4200ms | ✅ |
| ⑥ 鲸鱼消失、露出皮肤 | ~3450 – ~4200ms | ✅ 与⑤并行 |
| ⑦ UI 由外向内环环入场 | ~4300 – ~5300ms | ✅ |
| **总时长** | **≈ 5.6s** | 对齐官方桌面档 5.000s |
---
## 2. 逐帧截图证据
运行 `verify-headless.ps1` 会按采样点生成 `_verify/f*.png`(该目录已 gitignore,随时可重建)。对应关系如下:
| 文件 | 时刻 | 内容 |
|---|---|---|
| `_verify/f00950.png` | 950ms | 全黑,双线从左右边缘向中央延伸 |
| `_verify/f01300.png` | 1300ms | 双线已近乎接合,画面中央一道横线 |
| `_verify/f03300.png` | 3300ms | 鲸鱼轮廓完整可见(白色描边),中央横线已扫出,上下幕布开始退场 |
| `_verify/f03900.png` | 3900ms | 幕布基本退净,鲸鱼近乎消失,UI 三列已可见 |
| `_verify/f04300.png` | 4300ms | 舞台已移除;三栏处于入场位移中 |
| `_verify/f06000.png` | 6000ms | 稳定态,UI 完全就位 |
关键帧 `f03300.png` 可肉眼确认:**鲸鱼轮廓清晰可辨,且形状与侧边栏图标一致**(同一条 path)。
---
## 3. ⑦ 三层波次的目标捕获
在 mock 页(`[data-slot]` 均为 `display:contents`)实测:
```
[boot-anim] shot7 目标数 {cols: 3, inner: 5, deeper: 2}
```
- `cols: 3`:左/中/右三列全部命中
- `inner: 5`:列内主块
- `deeper: 2`:更内层内容块
**这一条是本次验证最有价值的发现**。修复前的实测数据是 `{cols: 3, inner: 0, deeper: 5}` —— `inner` 为 0,因为列的直接子元素是 `display:contents` 锚点(无盒子)。**静默失效,不报任何错**。加上 `collectBlocks()` 穿透后三层全部有目标。
---
## 4. 收尾断言(每次运行自动检查)
| 断言 | 实测 |
|---|---|
| 舞台已从 DOM 移除 | ✅ `stageRemoved: true` |
| 无 `[data-ba-anim]` 残留标记 | ✅ `markedLeft: 0` |
| 内建加载页已还原 | ✅ `bootPagePresent: false`(已被真实 UI 替换) |
| `html` 的 `overflow` 已还原 | ✅ `"(none)"` |
| 控制台零报错 | ✅ `LOGS: []` |
---
## 5. 无闪烁论证
需求要求「过渡到 UI 可交互状态无跳变、无闪白/闪黑」。本实现从构造上排除:
| 风险 | 为什么不会发生 |
|---|---|
| **闪白** | 动画全程幕布是纯黑 `#000`;揭幕瞬间露出的是**真实 UI**,其底色由 `body` 的 `var(--dsw-alias-bg-base)` 决定。而壳层 `createWindow` 用 `show:false` 等到 `ready-to-show` 才显示窗口 —— 窗口第一次可见时,页面已有内容,不存在「白底闪烁」窗口 |
| **闪黑(尾部)** | 幕布退场与鲸鱼淡出**并行**,它们在 ⑤⑥ 结束时同帧到达终态;紧接 ⑦ 的 UI 入场从位移 + 透明起步,不会出现「黑 → 白 → 内容跳入」的中间态 |
| **UI 抖动** | ⑦ 只动 `transform` 与 `opacity`,两者都在合成层,不触发布局与重绘(01 号文档 B2「Never animate layout properties」/ C3 MDN 结论) |
| **重复播放** | 注入 `script` 行只在前端 boot 流程执行一次;UI 之后的重渲染不会重放(满足 01 号文档 B1「One-shot only」) |
| **黑幕卡死** | 4 条安全网:`prefers-reduced-motion` 直接跳过 / 用户任何输入即跳过 / `finally` 必 `teardown` / 20 秒兜底定时器 |
---
## 6. 安装与生效验证步骤(需人工执行一次)
因为**重启桌面版会终止当前会话**,最后一步留给你:
```powershell
# 1) 装上
powershell -ExecutionPolicy Bypass -File \install.ps1
# 2) 重启「DeepSeek Harness」桌面应用
# 3) 观察:黑屏 → 中央鲸鱼 → 双线内合 → 轮廓勾勒 → 中线展开 → 落白 → UI 环环内移
# 4) 若需还原
powershell -ExecutionPolicy Bypass -File \uninstall.ps1
```
安装脚本会先备份 profile 的 `package.json` 为 `package.json.bak-boot-anim`,卸载脚本回滚依赖与 bundles 列表并删除 junction。
---
## 7. 真实 profile 安装验证(2026-10-03 12:53 补测)
安装脚本已在**真实 desktop profile** 上执行并通过验证。
### 7.1 安装结果
```
[1/5] ProfileDir = %USERPROFILE%\.dsh\profiles\desktop
[2/5] Backed up -> ...\package.json.bak-boot-anim
[3/5] dependencies += dsh-boot-anim -> link:
[4/5] bundles += dsh-boot-anim (now 25 entries)
package.json written (UTF-8 no BOM, 3733 bytes)
[5/5] Junction -> ...\node_modules\@local\dsh-boot-anim
```
### 7.2 Profile 完整性断言
| 断言 | 结果 |
|---|---|
| `package.json` 可被 Node `JSON.parse` | ✅ |
| 无 UTF-8 BOM | ✅ |
| bundles 数量 | 24 → 25 |
| 依赖数量 | 18 → 19 |
| 丢失的 bundle | ✅ 无 |
| 顶层键一致(dependencies / dsh / name / private) | ✅ |
| `patchReload` 保留为 live | ✅ |
| junction 建立、`lib/index.js` 可达 | ✅ |
### 7.3 用**真实加载器**验证 bundle 被接受
直接调用 `app.asar` 里的 `@deepseek-ai/dsh-app-boot` 的 `loadProfileDirectory()`:
```
layers 数量 = 25
skippedBundles 数量 = 0
找到 dsh-boot-anim: YES
packageDir = ...\node_modules\@local\dsh-boot-anim
patchPaths = ["...\cordis.patch.yml"]
patches = [{ "insert": [{ "id": "boot-anim", "name": "dsh-boot-anim" }] }]
VERDICT = PASS
```
25 个 bundle 全部被接受,**0 个被跳过**。
### 7.4 用**真实 cordis 运行时**跑端到端
加载 `app.asar` 里的 cordis,注册假 `webServer`,加载插件,触发 `collectIndexInjections()`:
```
插件 name = boot-anim | inject = ["webServer"]
注入行: 2 行
- style | placement = (default) | len = 1943
- script | placement = head | len = 17545
E2E = 12/12 ALL PASS
```
12 项断言全通过:style/script 行存在、placement 正确、CSS 作用域前缀、皮肤变量回退、reduced-motion 守卫、完整鲸鱼 path、七段分镜函数齐全、幂等守卫、两个半幅 clipPath、non-scaling-stroke。
### 7.5 修复的一个真实缺陷(安装时才发现)
**原 `install.ps1` 会弄坏 profile。** PowerShell 5.1 的 `Set-Content -Encoding UTF8` 会写入 UTF-8 BOM,而 BOM 会让 Node 的 `JSON.parse` 直接抛 `Unexpected token` —— 加载器读 profile 用的正是 `JSON.parse(readFileSync(path, "utf8"))`,一旦写入 BOM,**桌面版下次启动会读不出 profile**。
实测对比:
```
Set-Content -Encoding UTF8 -> 前 3 字节 239,187,191(有 BOM)-> Node JSON.parse 失败
WriteAllText(UTF8Encoding false) -> 前 3 字节 123,34,97(无 BOM) -> Node JSON.parse OK
```
现已改为 `[System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding $false))`,并在写入后**回读头三字节自检**,若为 BOM 立即抛错中止。`uninstall.ps1` 同样处理。
此外把 junction 的删除从 `Remove-Item -Recurse` 改为 `(Get-Item -Force).Delete()` —— 前者对 junction 会**穿透删除目标目录内容**,是危险操作。
---
## 8. 缓冲过场(④ 与 ⑤ 之间)
在鲸鱼轮廓勾勒完成、横线展开之前插入的一段过场,按确认后的分镜实现。
### 8.1 时序与实测
| 步 | 内容 | 实测时间窗 |
|---|---|---|
| ② | 鲸鱼淡出后重新组合 | ④ 收笔 + 240ms 起 |
| ③ | 鲸鱼左移让出中央 | +300ms 起,移出约 366px |
| ④ | 官方字标在其右侧浮现(位移 + 淡入) | 与左移同时收尾 |
| ⑤ | 后端未就绪则循环播斜向光带 | 直至就绪,**无次数上限** |
| ⑥ | 字标与光带收束 → 交回 ⑤⑥ 横线展开 | 就绪后约 300ms |
字标用的是上游 `BrandWordmark` 的官方矢量(18 个图元 / 13931 字符 path / 2 处裁剪框),
由 `_verify/gen-wordmark.mjs` 自动提取,不手抄也不会与图标不一致。
### 8.2 三种场景实测
**后端快(1.6s 挂载)**——过场完整播完再揭幕:
```
t=1900 鲸鱼完整,应用已挂载
t=3200 字标浮现中(markOp 0.60)
t=3600 光带起(后端已就绪,只闪一下即收)
t=4200 过场收束
t=5000 舞台撤除,UI 入场完成
```
**后端慢(6s)**——光带持续循环:
```
t=3600..5600 sheenOp 恒为 1.00,appMounted=0 ← 光带一直在扫
t=6200 后端就绪,字标与光带同帧收束
t=7400 舞台撤除
```
**后端很慢(12s)**——验证「无上限」:
```
t=3800..11000 sheenOp 恒为 1.00,appMounted=0
光带 transform 逐轮推进:6.7 → 90 → 170 → 220 → 23 → 103 → ...
共观测到 11 个不同取值,确认在连续扫动而非静止
t=12000 后端就绪
t=13500 舞台撤除
```
**插件加载失败**——过场播完后直接露出错误提示:
```
t=3200 bootVisible=0 过场进行中,加载页被压住
t=4400 bootVisible=1 舞台撤除,错误提示露出
画面显示 "Failed to load plugins" + 具体插件名
```
失败判定读的是 `[data-dsh-boot]` 元素内的文案(内建加载页失败时会写入失败标题),
比等超时更早、更准。
### 8.3 实现过程中修掉的两个真实缺陷
这两个都是**只有跑出来才会发现**的:
**① 光带不可见(两个叠加原因)**
第一版用 `mix-blend-mode: screen` 让光带叠在字标上。实测逐帧截图**字节数完全相同**——
画面上什么都没变。两个原因:
- 渐变用了 `gradientUnits="userSpaceOnUse"`,坐标 0→260,而光带矩形在 x=-160..100,
投影后正好落在渐变的全透明区间,**光带整个不存在**;
- 即便渐变对了,屏幕混合下 `screen(白, 白) = 白`,白色光带叠白色字标也**恒等于没有效果**。
改法:光带改为**遮罩**——字标本体压暗(`opacity: .32`),另叠一层同形的纯白副本,
由移动的光带遮罩裁切,只有扫过的部分高亮出来。这才是「一道亮带掠过字面」。
**② 鲸鱼与字标重叠**
初版把鲸鱼左移写死 180px,字标居中。实测截图显示两者压在一起。
改为**按实测宽度动态计算**:先定字标高度,再读鲸鱼 `getBoundingClientRect().width`,
算出「鲸鱼 + 间距 + 字标」整组居中时鲸鱼该在的位置。
1440 宽下校验:位移 -366px,组范围 354..1086,组中心 720 = 视口中心,间隙 44px。
### 8.4 一处按需求修正的设计
需求原文是「先**打开/展开**,随后画面**变为白色**」。我最初理解成要在展开之前
铺一层白场,结果造成「黑 → 白 → 黑 → UI」的闪烁,与「衔接无跳变」的验收标准相悖。
按需求顺序重读后确认:**「展开」与「变白」是同一个动作** —— DSH 界面本身就是白的,
展开露出皮肤的同时就完成了变白。因此删掉了多余的白场层。
### 8.5 未改动的部分
- **鲸鱼 path 的字节内容未动**(CI 有逐字节校验,实测通过)
- 上游的 ④ 轮廓勾勒、下游的 ⑤⑥ 展开与 ⑦ UI 入场均未改动
---
## 9. 逐帧诊断:两处视觉缺陷
用 `_verify/render-all-frames.mjs` 把过场按 50ms 步长全量渲染成 141 帧
(输出到 `_verify/allframes/`,含 `timeline.json` 与 `timeline.csv`),
再用像素扫描逐帧检视,发现并修掉两处缺陷,并顺带查出第三处。
### 9.1 缺陷一:鲸鱼与字标重叠
**现象**:鲸鱼左移、字标浮现的过程中,两者叠在一起。
**帧级证据**(`_verify/diag-progress.mjs`,逐 80ms 记录 `getBoundingClientRect`):
| 时刻 | 鲸鱼范围 | 字标范围 | 间隙 |
|---|---|---|---|
| 3366ms | 584..856 | 670..1086 | **−186** |
| 3463ms | 584..856 | 670..1086 | **−186** |
| 3635ms | 584..856 | 670..1086 | **−186** |
| 3729ms | 495..767 | 670..1086 | −97(鲸鱼开始左移) |
| 3914ms | 372..644 | 670..1086 | +26(脱离重叠) |
| 4182ms | 354..626 | 670..1086 | +44(终态) |
**成因**:字标与鲸鱼左移**同时启动**,但字标 520ms 就到位,鲸鱼左移 620ms
且延迟 540ms 才开始。字标先出现、鲸鱼还停在中央,于是重叠约 350ms。
**位移公式本身是对的** —— 终态实测间隙恰好 44px,与设计值 `T.MARK_GAP` 一致。
问题只在入场时序不同步。
**结论**:缺陷,**已修**。
**修法**:字标的浮现延迟从固定的 `MARK_IN_DELAY`(180ms)改为
「鲸鱼左移进行到 82% 时」才启动 —— 此时鲸鱼已让出绝大部分空间,
字标再淡入就不会叠上,同时两个动作仍有短暂交叠,不会显得脱节。
入场总时长相应改为 `max(左移结束, 字标浮现结束)`,避免 gate 提前放行。
**修复后实测**(`_verify/probe-overlap.mjs`,逐 60ms 采样):
| 时刻 | 鲸鱼 | 字标 | 间隙 |
|---|---|---|---|
| 4235ms | 354..626 | 670..1086 (op 0.40) | +44 |
| 4560ms | 354..626 | 670..1086 (op 1.00) | +44 |
| 4904ms | 354..626 | 670..1086 (op 0.27) | +44 |
| 6215ms | 354..626 | 670..1086 (op 0.22) | +44 |
最小间隙 **44px**(恰为 `T.MARK_GAP`),**重叠帧数 0**。
**连带修掉一处**:改完时序后发现字标会先淡入到全白、再「啪」一下跳到压暗值。
原因是压暗是瞬间生效的。已给压暗加 260ms 过渡,实测不透明度
`1.00 → 0.98 → 0.72 → 0.27 → 0.22` 平滑下沉。
### 9.2 缺陷二:左上角出现疑似「第一页」的图形
**现象**:鲸鱼左移完成后,左上角出现一块很大的图形。
**帧级证据**(左上角 x 0..400 / y 0..120 非背景像素扫描,全量 141 帧):
| 帧 | 非背景像素 | 灰度范围 |
|---|---|---|
| 4307ms | 7113 | 0..67 |
| 4359ms | 9481 | 0..200 |
| 4402ms | 7590 | 0..233 |
| 5902ms | 10023 | 0..255 |
肉眼查看该区域:4307 帧显示 `dee`、4402 帧显示 `eeps`、5902 帧显示 `deer` ——
**是放大失控的 `deepseek` 字标被光带裁切出的片段**,不是「第一页」,
也不是任何汉字。放大后被裁成两半,因此形似汉字。
**几何实测**(`_verify/diag-sheen.mjs`):
```
修复前:
.ba-mark (字标本体):416 × 64 @ (670, 418) ← 居中
.ba-sheen (光带层) :1440 × 222 @ (0, 0) ← 铺满左上角
尺寸比 3.46×
```
**成因**:两层 SVG 用同一个 viewBox 与同一份 18 条 path,但 CSS 定位规则不同 ——
`.ba-mark` 是居中 + `height: var(--ba-mark-h)`,而 `.ba-sheen` 写的是 `inset: 0`
(撑满容器)。于是光带层里的字标副本被放大到 1440 宽、锚定在 (0,0)。
**排除项**:在整条 DSH 前端产物里搜索 `第一页` / `Page 1` / `pagination` 全部未命中,
确认该图形完全出自本插件的 `.ba-sheen` 层,与 DSH UI 无关。
**修法**:
1. `.ba-sheen` 的定位改为与 `.ba-mark` 完全一致;
2. `--ba-mark-h` 从 `mark` 改设到 `stage`(两层是兄弟节点,只有父级变量才能同时继承);
3. 光带层同步字标的 `markShift` 位移。
**修复后实测**:两层均为 416 × 64 @ (670, 418),重合判定为「是」;
全量 141 帧的左上角**非背景像素归零**。
**结论**:缺陷,已修。
### 9.3 顺带查出:扫光几乎不可见
修完定位后发现扫光效果极弱 —— 字标区亮像素只从 7300 微涨到 7587(+4%),
逐帧像素差 >50 的仅 4 次。
**成因**:字标本体与光带高亮层都是纯白,`白 + 白 = 白`。
虽然代码里有「加载期间压暗字标」的逻辑,但**没生效** ——
实测 `inline opacity = 0.22` 而 `computed opacity = 1`:
```
时刻 computed inline
4308 1 0.22 ← 内联样式被无视
```
原因是 `.ba-mark` 上有入场动画且带 `fill: 'forwards'`,
**Web Animations 的动画值优先级高于内联样式与类选择器**。
**修法**:压暗也改用 WAAPI 动画(再叠一层 `opacity: 0.22` 的替换动画),
退场时 `cancel()` 让入场动画的终值恢复。
**修复后实测**:`computed opacity = 0.22` 生效;字标区亮像素起伏从
`0..1161`(此前 7300..7587),逐帧像素差 >50 的次数从 4 次升到 **12 次**。
**结论**:缺陷,已修。
### 9.4 复现命令
```powershell
# 全量渲染(141 帧,50ms 步长)
node _verify/render-all-frames.mjs 50 12000 7000
# 三项诊断
node _verify/diag-sheen.mjs # 两层几何是否重合
node _verify/diag-opacity.mjs # 压暗是否生效
node _verify/diag-progress.mjs # 鲸鱼与字标是否重叠
```