# 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 += @local/dsh-boot-anim -> link: [4/5] bundles += @local/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 找到 @local/dsh-boot-anim: YES packageDir = ...\node_modules\@local\dsh-boot-anim patchPaths = ["...\cordis.patch.yml"] patches = [{ "insert": [{ "id": "boot-anim", "name": "@local/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 # 鲸鱼与字标是否重叠 ```