# Changelog

本项目的版本记录。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)，
版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。

## [1.0.1] — 2026-10-10

- 📝 文档：**致谢内置素材的作者**——HDD 开机动画《Hollow Deep Dive System》由 B 站 UP 主
  [@露霁luki_yo](https://space.bilibili.com/3537104783018790) 授权提供，《素材与许可》《致谢》两节都写明来源与链接。
- 📝 同步更新英文 README（原来还停在 0.2.x 的三档开关描述）。
- 代码没有任何改动（49 项测试照旧全过）。

## [1.0.0] — 2026-10-10

首个正式版：功能定型（自带素材 + 导入自己的视频 + 自动转码 + 设置页一页管完），
测试 49 项全过；发布形态只带已授权的 HDD 那段素材。

## [0.9.2] — 2026-10-10

- 🔧 **素材按"包里真有哪个文件"为准**（用户实测反馈：「这两个选项（HDD和月读世界，都指向HDD的素材），
  你没塞月读的素材，那倒是把月读世界的选项删掉啊」）。发布包只带 `hdd.mp4`，可宿主的素材表里还写着
  `yuedu` → 客户端画出「月读世界」按钮、点下去只落到 HDD。现在宿主启动时**逐个检查素材文件是否存在**，
  不存在的**既不列给客户端、也不注册路由**；界面说明文字也跟着实际素材动态生成。
- 🔧 **修一个会让 HEVC 漏过去的真 bug**：宿主探编码时写死了旧的 ffmpeg 路径
  （`node_modules/@ffmpeg-installer/...`），而 0.9.0 起插件会自己把 ffmpeg 下到 `bin/ffmpeg.exe`——
  于是"已经下好 ffmpeg 的用户"导入 HEVC 时，探测以为没装 ffmpeg 而**放行**（黑屏一闪的老毛病复发）。
  现在统一走 `lib/transcode.js` 的 `ffmpegPath()`。
- 🩺 **素材清单与版本号写进首帧脚本**（不再依赖单独一行的 `kind:'global'` 注入——那条在 Desktop 载体上
  观察不到）。附带：某段素材加载不了、但还有别的可用时，**自动换一段继续播**，不弹报错卡片。
- 🧪 **测试救活两条**：ffmpeg 候选路径过时（真正的 ffmpeg 在**工作区根**的 node_modules，插件自己下的在
  `bin/`），于是"只收 mp4 / 转码往返"两条用例一直被跳过、真 bug 就藏在那后面。现在 **49 项全过、0 跳过**。

## [0.9.1] — 2026-10-09

- 🔧 **修布局溢出**（用户截图反馈：「"播放一遍"的按钮都出线了」）：素材带 + 导入 + 播放原来挤在同一行，
  设置面板只有 800px 宽、素材一多就把右边按钮挤出面板。现在：
  - **素材带独占一行**（会换行），**操作按钮另起一行右对齐**；
  - 素材按钮的**文字用 CSS 截断**（`max-width:132px` + 省略号），名字再长也不会挤走按钮；
  - 导进来的片子名字在素材带里只显示前 8 个字（完整名字在 tooltip 与下面的管理列表里）。
- 测试：**48 项全过**（布局是纯 CSS/DOM 结构调整，断言里那两条"按钮在同一行"也跟着改成新结构）。

## [0.9.0] — 2026-10-09

**两件大事：能自动转码了；开机不再跟界面抢资源。**

- 🎬 **自动转码（导入真正的兜底）**：导入时如果宿主发现这段视频浏览器解不了（典型是
  HEVC / H.265），**不再直接删掉**——它把那段文件留着，界面弹一句「这段视频浏览器解不了它的编码，
  要不要自动转换？」，点一下就：
  1. 宿主**自己从国内镜像下载 ffmpeg**（28MB 左右，一次就够；只允许本机回环请求触发），
  2. 用 `libx264 -crf 20 -pix_fmt yuv420p -aac -faststart` 转成能播的 mp4，**只转视频、保留原分辨率与帧率**，
  3. 转好**先验一遍能不能播**，再替换原文件、进清单、自动选上、播一遍、弹起名窗。
  进度条会显示"下载 ffmpeg / 转码百分比"，转码状态由 `GET …/import/convert-status` 轮询。
  这也是用户 2026-10-09 卡住的那一步：他的 `欢迎来到月读世界（原版）.mp4` 就是 HEVC。
- 🩺 **报错不再丢信息**：宿主把「给用户看的那句话」直接放在 `error` 字段里（以前放的是
  `codec-unsupported` 这种机器码，客户端某些版本只显示它 → 用户只看到"HTTP 422"）。
  所有拒绝理由（不是 mp4 / 超 512MB / 没视频轨 / 编码不支持 / 写盘失败）现在都是人话。
- ⏱️ **启动不与界面抢资源**：应用启动时片头**默认延 600ms 再起播**，让 DSH 自己的客户端模块
  先把界面铺出来（4K 片头一动，解码 + 磁盘读 + 音频会跟模块加载抢 CPU/IO）。
  ✅ 不牺牲体验：**等待期间视频一旦就绪（`readyState >= 3` 或 `loadedmetadata`）就提前开播**，
  多数机器上察觉不到。想一出即播：`localStorage['dsh-boot-video:start-delay'] = '0'`。
  设置页的「播放一遍」和换素材**永远立刻播**，不受这个延时影响。
- 🩹 **选中的素材没了就自己退回去**：用户选过一段导入的视频、之后那段被删或丢了，浏览器还记着它
  → 每次开机都去请求不存在的文件 → 弹"打不开"卡片。现在遇到这种情况**悄悄换回默认素材并同场重播**，
  不拿它烦用户（2026-10-09 用户就这么被烦了一次）。
- 🧪 **真机实测**（用户那段 57.2MB 的 HEVC《欢迎来到月读世界（原版）.mp4》，在跑着的桌面版宿主上
  走完整流程）：上传 1.1s 得到**人话版 422**（`codec-unsupported` + `salvageable` + id）→ 转码 **94s**
  （57.2MB → 48.1MB，保持 3840×2160 / 59.94fps / 24.28s）→ `HEAD` 该素材 200、
  `content-type: video/mp4`、`accept-ranges: bytes`。
- 测试：新增 5 条（转码岔路的完整交互、真实 HEVC → H.264 的转码往返、启动延时的两条、
  选中素材丢失时的自动回退），全仓 **48 项全过 / 0 跳过**。

## [0.8.0] — 2026-10-09

- 🔧 **修一个我自己的判断失误：「导入自己的视频」默认重新打开**（用户原话：「我最重要的导入视频的
  功能呢」）。0.6.0 我把那句「插件里基础开屏动画只有 HDD 和月读世界这两个我最开始装的就可以了、
  不需要其他的」理解成了"把导入砍掉"，其实他说的是"**内置**就这两段够用"——导入是这个插件的
  主功能之一，不该藏起来。
  - 宿主侧开关改成 `DSHBV_IMPORT !== '0'`（**默认开**，想关就设 `DSHBV_IMPORT=0`）
  - 客户端 `IMPORT_ENABLED = true`（那套界面、素材带里的导入片子、自命名弹窗全部回来）
  - 测试里 `CLIENT_IMPORT_ENABLED = true`：**5 条被跳过的导入界面用例全部恢复**，
    另加一条"`DSHBV_IMPORT=0` 时只剩三条素材路由"的回归——43 项**全过、0 跳过**。
- 发布包不受影响：`npm run pack:release` 仍然只带已授权的 HDD 那段；**导入进来的视频在用户自己
  机器上**，不会跟着包走。

## [0.7.0] — 2026-10-09

**发布前的一轮加固**（HDD 那段已获 B 站 UP 主授权；月读世界那段没有授权，所以不进发布包）：

- 🩺 **设置页多了自检行**：客户端半边**加载时刻**（刷新/重开都会更新——重开了还是旧样子就说明
  页面没真正重新加载）、**上次开机结果**（正常播完 / 被跳过 / 声音被拒静音 / 打不开+原因）、
  宿主那一半的版本号。以前这些只能翻控制台，普通用户根本看不到。
- ⚔️ **防打架提示**：检测到 550C 开机动画也开着时，设置页直接说明"两个都是全屏遮罩、会叠着播"，
  点一次「我的视频」就顺手把它关掉（本插件唯一一次写别人的键，且只在它的值不是 `off` 时写一次）。
- 📦 **发布包只带已授权素材**：新增 `npm run pack:release` —— 打之前临时把月读世界那段排除、
  打完还原清单，并**核对真包内容**（`npm pack --dry-run --json` 会报旧清单，所以一律以 tar 里的
  文件名判定；混进没授权素材就删包并非零退出）。当前发布包 **11.37 MB**，只有 `assets/hdd.mp4`。
  宿主侧配套：`DSHBV_ASSETS=hdd` 时**只注册 HDD 那条路由**，并把 `__dshBootVideoAssets` 注入给
  客户端——没提供的素材在界面上连按钮都不会出现。
- ✍️ README 补上**卸载**、**桌面版走插件管理器安装**、**自打发布包**三节；素材授权状态列成表。
- 测试：新增 4 条（体检行写入与显示、550C 提示与顺手关闭、宿主的素材白名单与注入、
  没提供的素材不画按钮），全仓 **42 项：37 通过 / 5 跳过 / 0 失败**。

## [0.6.0] — 2026-10-09

**回到"只有内置两段"的形态**（用户拍板：「插件里基础开屏动画只有 HDD 和月读世界这两个我最开始装的
就可以了，不需要其他的」）：

- 设置页不再显示「导入自己的视频」那块：素材带里只有 **HDD / 月读世界**，右边只有「▶ 播放一遍」——
  **插件自带的就是这两段**，别人的视频不该跟着包走。
- 导入那一整套（上传、按路径复制、自命名弹窗、清单 / 改名 / 删除）**代码一个字没删**，改成两盏开关：
  - 客户端：`lib/client.js` 的 `IMPORT_ENABLED`
  - 宿主：环境变量 `DSHBV_IMPORT=1`（读它才注册那四条路由；不设就是干净的只有素材路由）
  两个都打开，界面与接口一起回来。
- 认不出的素材 id（比如以前导入过的）在关掉导入的版本里回落到默认素材，不留"选中了一段界面上
  没有的东西"的幽灵状态；`assets/user/` 已清空，包里只剩 `hdd.mp4` 与 `yuedu.mp4`。
- 测试：新增"默认形态下不注册任何导入路由"与"DSHBV_IMPORT=1 才挂上"两条；导入界面那 5 条用例
  跟着开关跳过（`CLIENT_IMPORT_ENABLED`），**全仓 37 项：32 通过 / 5 跳过 / 0 失败**。

## [0.5.0] — 2026-10-09

**设置页按用户要求重排**（他的原话：「我想把这个界面优化一下上下两个区域合并，把导入视频的按钮放在黄色框的位置，
每导入一个视频，随即就主动弹窗让其自命名，然后就可以放在黑框位置供选择」）：

- **上下两个区合并成两个上下相邻、职责清楚的卡片**：上面「开关 + 片头素材」，下面「我导入的视频（管理）」。
  导入按钮不再单独占一行。
- **「⬆ 导入视频」挪到素材那一行的右边**，跟「▶ 播放一遍」并排；导入进来的片子**直接进左边的素材带**，
  跟内置的 HDD / 月读世界并列——点一下就换，不用再去下面找。
- **导入完立刻弹起名窗口**（居中小窗 + 遮罩，遮罩挡住覆盖层、不会误触「跳过」）：草稿预填原始文件名，
  回车/「就这么定」保存，「先用原名」跳过。原来的"列表里就地变输入框"改成这个弹窗；
  列表里的「改名」也走同一个窗口。
- 素材带里每条导入的片子后面挂一个小「×」，就近删除（照样只删插件里那一份）。
- 测试：改/加 3 条（导入按钮与素材带同一行、导入完弹出起名窗口、改名走弹窗且素材带同步换名），
  全仓 **36 项通过**。

## [0.4.4] — 2026-10-09

- **只收 mp4**（用户拍板：「看不懂，不行就写死只能导入mp4」）：选择器改成 `accept=".mp4,video/mp4"`，
  并且**两道关都卡**——①扩展名必须是 `.mp4`（不是就 415，客户端在本地就先把话说清楚，不白传）；
  ②里面必须是能播的编码（H.264 / VP9 / AV1；HEVC 这种 422 拒掉）。
  这样"我该准备什么文件"只有一个答案：**mp4 + H.264**。
- 之前"按编码白名单收"能收 mkv/webm，但对用户来说不可预判——他手上一个 mkv 就撞了墙，
  所以宁可把口径收窄到他一眼能记住的程度。
- 设置页那行提示同步改成「只收 mp4（H.264）文件，上限 512 MB……mkv 之类要先转成 mp4」。
- 测试：新增"非 mp4 一律 415"与"本地就拦 mkv"两条断言，全仓 **35 项通过**。

## [0.4.3] — 2026-10-09

**三个真机 bug，全是用户那边先炸出来的（这一段是当天的事故复盘）**：

- 🔴 **用户素材永远 404 → 开机只看到"黑屏一闪"**：前缀路由写成了 `/dsh-boot-video/asset/`
  （带尾斜杠），而 DSH 的前缀匹配是 **`/` 边界语义**（`pathname === prefix || startsWith(prefix + '/')`），
  带尾斜杠就永远匹配不上。内置素材走 exact 所以没事，用户素材全挂。
  修：前缀只写到 `/dsh-boot-video/asset`。⚠️ 我自己的小测试服务器用的是朴素 `startsWith`，
  所以单元测试是**假绿**——这类"路由语义"必须拿真宿主验。
- 🔴 **"打不开"卡片把整个桌面版锁死**（用户连主界面都进不去）：`showPlaybackError` 先立
  `finished = true` 再画卡片，而退场走的是 `finish()`，被自己的守卫挡住 → 按钮和自动退场全是空转。
  修：退场拆出独立的 `dismiss()`，它**不看 `finished`**。
- 🔴 **卡片仍然退不掉**：卡片"自己停留 6 秒"的计时器和淡出**共用 `fadeTimer`**，
  卡片一画就把"正在退场"的标记占住，`dismiss()` 又把自己挡回去。修：两者分字段（`cardTimer` / `fadeTimer`）。
- ✨ **导入那一刻就拦住不能播的编码**：宿主在文件落地后、写进清单前用 ffmpeg 探一次视频轨，
  非 H.264 / VP9 / AV1（典型是 HEVC / H.265，Chromium 解不了）→ 删掉刚落地的文件、返回 422 与一段人话。
  没装 ffmpeg 就放行（那是"更好"，不是"能用"）。
- ✨ 打不开时不再"黑屏一闪"：留一块说明卡片（写清可能原因 + 怎么绕过），点按钮或 6 秒后自己退场。
- 新增 `scripts/import-hevc.mjs`：把 HEVC 母带转成可播的 H.264（`-crf 20 -pix_fmt yuv420p
  -c:a aac -movflags +faststart`），直接把产物放进 `assets/user/` 并打印素材 id。
- 测试：新增 2 条（卡片必须能退场 × 2）并把 `FakeElement` 的 `dispatch` 改成**真会冒泡**的模型
  （`stopPropagation` 真的挡住），另外把"前缀不能带尾斜杠"写成断言——全仓 **35 项通过**。

## [0.4.1] — 2026-10-09

- **可以给自己导入的视频起名字了**（用户要求：「加一个自命名」）：列表里每条素材旁边多了「改名」，
  点开是个输入框（回车保存 / Esc 取消），名字存进 `assets/user/labels.json`。
  **只改显示名——磁盘上还是 `u-*.mp4`**，所以改名永远动不到正在用的素材、也改不坏播放路径；
  名字上限 40 字，留空 = 清掉自定义、回到原始文件名。
- 新增 `POST /dsh-boot-video/import/rename`（body `{ id, label }`）；名字表**先写临时文件再改名**，
  断电也不会留下半截 JSON；删素材时顺手清掉它那条名字（表不会越用越长）
- 名字表读坏（不是 JSON / 不是对象）一律降级成空表——名字只是显示用，不该因为它让整页出不来
- 测试：补 2 条（宿主侧"改名不动磁盘文件名、空名字回到原名、超长截断、幽灵 id 404"，
  客户端侧"改名走 rename 接口、列表原地刷新、取消不动数据"），全仓 **32 项通过**

## [0.4.0] — 2026-10-09

- **可以导入自己的视频当片头了**（用户原话：「我的这两个开机动画就塞到插件里面，至于其他人导入的，
  也给他设置成塞到插件里面的文件夹里」「我想把这个插件做成所有人安装后，可以自己去选择自己心仪的
  视频片段导入其中」）：设置页里「⬆ 导入视频」挑一段自己的 mp4，存进插件的
  `assets/user/`，和内置的 HDD / 月读世界并列在同一个列表里，随时能选、能预览、能删。
  上限 512 MB，**不转码、不重封装**——导进去什么样，播出来什么样。
- **网页版和桌面版都能导**（用户要求「网页版和桌面版都可以做到」）：
  - 网页版：把文件本体**流式 POST** 给宿主（XHR，带上传进度条），宿主边收边写盘，不占内存
  - 桌面版：Electron 的 `getPathForFile`（DSH 把它暴露成 `__DSH_HOST_PATHS__.pathFor`）能把
    用户选的文件翻成绝对路径，于是**宿主自己复制**，几百 MB 不必经过渲染进程；
    这条路不通（旧 Electron、跨盘、没权限）会自动回退到上传那条
  - 两条路都用**相对路径**打宿主路由（`dsh-boot-video/import…`），所以网页版打在 3080、
    桌面版打在 19387，同一份代码不用改
- 新增四个宿主接口：`POST /dsh-boot-video/import`（上传）、`GET /dsh-boot-video/imported.json`
  （清单）、`POST /dsh-boot-video/import/delete`（删除）、`POST /dsh-boot-video/import/from-path`
  （按路径复制，**仅回环可用**，且要求客户端算出的 `size-mtime` 指纹对得上，防止导入一个被换过的文件）
- 用户素材走**前缀路由** `/dsh-boot-video/asset/`（id 形状 `u-<base36 时间>-<序号>`，
  白名单校验顺带挡路径穿越）；清单是**列目录推导**出来的，不另存 json，少一处可能不一致的状态
- 删除只删插件里那一份，**绝不碰用户的原片**；删掉的正好是当前选中的，就退回默认素材
- 修掉两处会让功能"看起来没生效"的坑：① `readAsset()` 原本只认内置 id，导入的素材一重启就
  悄悄掉回 HDD；② 导入按钮原本是 `<label>` 转发点击，**Electron 里不触发 file input**，
  改成按钮显式 `input.click()`
- 测试：新增 8 条（上传契约、桌面版快路与回退、错误文案、清单渲染、导入后自动选中并重播、删除回退），
  宿主侧 6 条（上传落盘字节一致、清单、能播、越界 id 404、超限 413、按路径复制的指纹与回环校验）；
  全仓 30 项通过

## [0.3.0] — 2026-10-09

- **设置从「通用设置」里搬出来，在设置侧边栏单开一页「开机动画」**（用户原话：
  「我想把在黄色框区域内单开一个用来修改回框里面的内容」）：原来只是一行
  `settings.general.item`，现在整页归自己——`settings.section` + `id: boot-video`、
  `order: 25`，页面里分「启动时播放 / 片头素材 / 预览」三块，不再和别的设置挤在一起
- **和 550C 彻底解耦**（用户原话：「我的插件不要带上 550C 的插件了」）：
  - 设置里删掉「550C 片头」这一档，开关只剩「我的视频 / 不播放」
  - 不再为切换去写 `localStorage['dsh-550c-boot:mode']`
  - **遗留清理**：启动时把 0.2.0 及更早写下的 `'off'` 抹掉（那个键留着会让 550C
    永远不播，用户会以为它坏了）；老开关里的 `'550c'` 迁移成 `'off'`（当初选它的人
    就是不想看我们的动画，不能被掉包成默认的视频）
- **修掉「素材按钮看不出选中」**：0.2.0 的 CSS 用了本主题里并不存在的 token
  （`--dsw-alias-text-primary` / `--dsw-alias-fill-tertiary` / `--dsw-alias-fill-primary`），
  每次都掉进硬编码兜底色，选中态几乎不可见。现在只使用真实存在的 token
  （`label-primary/secondary/tertiary/caption`、`border-l1/l2/l3`、
  `interactive-bg-hover`），底色一律 `color-mix(in srgb, currentColor …)`，
  选中态再加一个 `✓` 前缀——深浅主题都不靠肉眼猜
- 预览按钮从「有就不动」改成**每次都从头重播**（改叫「▶ 播放一遍」），换素材/
  拨回「我的视频」也是立刻重播一遍，用户不用为了看效果重启
- 页面底部显示插件版本（读宿主注入的 `window.__dshBootVideoVersion`），
  并写明"不播放"时如何一键恢复
- 测试：新增 5 个设置页用例（注册契约、开关落点、换素材重播、预览重播、
  从 storage 回读选中态）与 3 个遗留清理用例；`client.js` 导出 `__internals` 供测试
  直接验证"点了之后键写对没有"

## [0.2.0] — 2026-10-09

- **两段片头可切换**：素材表 `ASSETS = ['hdd', 'yuedu']`，路由改成 `/dsh-boot-video/asset/<id>.mp4`
  （旧路径 `/dsh-boot-video/boot.mp4` 保留并指向默认素材——没重启的客户端 bundle 不会因此断掉）；
  设置行新增「素材」一组，切换后立刻用新素材重播一遍
- 新增素材 `assets/yuedu.mp4`（"欢迎来到月读世界"）：源片 3840×2160 / 59.7fps / 52.7Mbps，
  重压为 **29.01 MB / CRF 24 / 保持 4K 60fps**，PSNR 平均 **46.2 dB**
  （同时测了 CRF 22 = 47.5 dB / 35.3 MB、CRF 18 = 49.8 dB / 52.1 MB——三档都属视觉无损，取了体积最小的）
- 原素材改名 `assets/boot.mp4` → `assets/hdd.mp4`，并留一个 `boot.mp4` 硬链接给旧代码路径

## [0.1.0] — 2026-10-04

首个版本。

- 视频开机片头：全屏播放 `assets/boot.mp4`，播完淡出进界面，点击画面 / `Esc` 跳过
- 宿主半边在文档解析阶段注入首帧（纯色 `#000000`），抢在 DSH 开机卡片之前占屏；
  客户端覆盖层挂上后同任务调 `__dshBootVideoFirstFrame.end()` 完成交接
- 宿主按 `webServer.register({ kind: 'exact' })` 提供 `/dsh-boot-video/boot.mp4`，
  支持 Range / 206 / 304 / HEAD（流式，不把视频读进内存）
- 声音：**桌面版（Electron）自动带声音播放**；网页版被浏览器拦下未静音自动播放时，
  自动降级成静音并亮出「打开声音」按钮。同一份代码两边各得其所，不需要两份插件
- 页面不可见时的兜底：Chromium 在后台**不会给 `<video>` 加载任何数据**（`play()` 成功返回、
  `readyState` 恒为 0），回到前台也不会自动恢复——现在监听 `visibilitychange`，
  回到前台时重新挂源并再播一次；watchdog 在"页面一次都没露过面"时也不判死，只重新计时
- 起播容错：`play()` 被拒（窗口在后台、页面被 Chromium 节流时连静音都会被拒）不再直接收场，
  而是隔 800ms 重试至多 5 次；`AbortError` 忽略，重试用尽留着覆盖层交给 watchdog
  （踩过的坑：旧写法一被拒就 finish()，覆盖层自己撤掉、开机卡片露出来，桌面版还会判定
  这个 entry 没激活、弹「应用无法启动」）
- 设置 → 通用 → 开机动画：三态开关（我的视频 / 550C 片头 / 都不播）+ 预览
- 画面不做裁切（`object-fit: contain`）：中途做过「铺满」档，用户看过两档后要求删掉——
  16:9 窗口播 2.35:1 的片子，铺满要裁掉左右各约 12%，那是画面少了一块
- 素材流水线 `scripts/make-asset.mjs` 支持 `--crop W:H:X:Y`，用来裁掉片源自带的黑边；
  `scripts/fetch-ffmpeg.mjs` 走国内镜像一条命令拿到可用的 ffmpeg
- 与 dsh-550c-boot 二选一：写 `localStorage['dsh-550c-boot:mode']`，不改对方代码
- 绝对兜底：`duration` 读不到或视频出错时的 watchdog，任何出口都走同一段淡出
