Bundle
dsh-inju-boot
DeepSeek Harness 启动动画插件:每次启动 Harness 时全屏播放 INJU 3D 粒子启动动画(WebGL2 · 6 万粒子 · 花 → 月球 → 虚影文字 → 实体 LOGO · 极客 HUD),带背景音乐自动播放、顶栏融入动画与一整页设置。
- Source
- inju0524
- License
- MIT
- Updated
- Updated 7 days ago
Readme
<div align="center">
<img src="icon.svg" width="112" alt="INJU 启动动画">
# dsh-inju-boot · INJU 启动动画
**给 DeepSeek Harness 装一支开机动画。**
每次启动 Harness,全屏播放一支 WebGL2 实时粒子动画 —— 花 → 月球 → 虚影文字 → 实体 LOGO,
带一套极客 HUD / 终端层和 Apple 发布会式的程序化音效,播完自动淡出把界面交还给你。
[](https://github.com/inju0524/dsh-inju-boot/actions/workflows/test.yml)
[](LICENSE)
[](package.json)
[](package.json)
[快速开始](#快速开始) · [设置项](#设置项) · [工作原理](#工作原理) · [自检](#自检) · [排障](#排障) · [English](#english)
</div>
<p align="center">
<img src="docs/preview-stages.gif" width="720" alt="INJU 启动动画 · 10 个阶段">
<br>
<sub>6.3 秒走完全部 10 个阶段(每段约 0.6 秒,硬切)· 720×405 · 4 MB</sub>
</p>
### 十个环节
| # | 阶段 | 时间 | 画面 |
|---|---|---|---|
| 01 | 汇聚 | 0 – 3.2s | 星尘云团被吸聚成一朵参数化 3D 雏菊(花瓣 / 花心 / 花托 / 花茎 / 地面亮点) |
| 02 | 花体 · 环绕巡游 | 3.2 – 5.8s | 相机缓慢环绕,花瓣层次与花茎前后关系都看得到 |
| 03 | 崩解 | 5.8 – 7.3s | 每颗粒子沿花心向外爆散,带湍流冲量与惯性拖尾 |
| 04 | 重聚 | 7.3 – 9.3s | 回吸聚合成一颗 3D 粒子月球(撞击坑 / 月海 / 山脉) |
| 05 | 月球 · 静默自转 | 9.3 – 11.7s | 坑谷环脊在光照下缓缓转出 |
| 06 | 显影 | 11.7 – 14.4s | 月球 → 虚影文字,粒子变细、变弥散、开始抖动 |
| 07 | 虚影 · 闪烁漂移 | 14.4 – 15.7s | 全息虚影闪烁、漂浮 |
| 08 | 像素偏移闪切 | 15.7 – 16.45s | 28 条横向切片错位 + 13×11 块状位移 + RGB 分离 + 过曝闪白 |
| 09 | 实体 · 平面 LOGO | 16.45 – 20.05s | 硬切为实体平面 LOGO,带扫光;残留粒子成为光晕 |
| 10 | 循环 · 淡出 | 20.05 – 21.05s | 淡出并重新开始 |
> 全部 10 个阶段的时长、手感、每阶段的粒子尺寸 / 抖动 / 爆散冲量 / 体积光都能在设置页调;
> 上面这张表读的就是动画目录那份 `inju.config.js`。
---
## 这是什么
一个 DeepSeek Harness 插件(bundle),给 Web 界面加两样东西:
| | |
|---|---|
| **启动覆盖层** | 挂在 `shell.overlay`,页面一加载就全屏播放动画,播到设定位置后淡出 |
| **设置页** | 挂在 `settings.section`,与宿主其它设置页并排,动画的一切参数都在这里调 |
动画本体是一支**零依赖的单文件 WebGL2 作品**:6 万颗自发光粒子、10 个阶段、程序化生成的
3D 雏菊 / 粒子月球 / 虚影文字 / 实体 LOGO,外加一层画在画布内、会跟着一起吃泛光与像素偏移闪切的
**极客 HUD**(数字雨、跟随主体的目标框、扫描线、实时点云小图、终端日志、遥测、hex dump)。
它还带了 **DeepSeek Harness 的产品身份层**:顶部逐字拉开的 `DEEPSEEK HARNESS` 铭牌、
左上角的 `root@deepseek-harness:~# dsh --profile desktop`、遥测里的 `MODEL` / `RUNTIME` 字段、
以及一整套 DSH 启动序列的终端日志。
<p align="center">
<img src="docs/preview-flower.webp" width="440" alt="花体阶段(完整分辨率)">
<img src="docs/preview-logo.webp" width="440" alt="实体 LOGO 阶段(完整分辨率)">
<br>
<sub>完整分辨率静帧:第 02 阶段 · 花体环绕巡游 | 第 09 阶段 · 实体平面 LOGO</sub>
</p>
它还带了 **DeepSeek Harness 的产品身份层**:顶部逐字拉开的 `DEEPSEEK HARNESS` 铭牌、
左上角的 `root@deepseek-harness:~# dsh --profile desktop`、遥测里的 `MODEL` / `RUNTIME` 字段、
以及一整套 DSH 启动序列的终端日志。
```
y= 27 root@deepseek-harness:~# dsh --profile desktop PHASE 2 / 10 花体 · 环绕巡游
y= 29 D E E P S E E K H A R N E S S
y= 55 [ok] cordis composition online · sandbox: workspace-local 2.00 / 21.05 s
y= 63 agent runtime · cordis composition · profile desktop
y= 83 [!!] approval: never · telemetry: local · sig 0x000E 60,000
y= 194 MODEL deepseek-v4.1-flash
y= 222 RUNTIME dsh 0.1.7 · node 22
y= 296 TARGET FLOWER · LOCKED
y= 804 [boot] deepseek-harness 0.1.7 :: profile desktop
y= 935 [███░░░░░░░░░░░░░░░░░░░░░░░] 10% DEEPSEEK HARNESS
```
> 上面是 `node tools/check-animation.mjs --dump` 打出来的**真实绘制调用**(1920×1080,t=2.0s)。
> 想看你自己屏幕尺寸下的排版:`node tools/check-animation.mjs --dump --w=1440 --h=900 --t=9.5`
<details>
<summary>预览图 / 动图是怎么来的(想自己重新生成的话)</summary>
动画自带**静帧模式**:`?t=<秒>` 定格在指定时刻并暂停,`hud=0` 隐藏页面 DOM 装饰、`telemetry=1` 保留极客 HUD。
插件把动画挂在 `/api/inju-boot/boot.html`,所以只要 Harness 在跑,直接用浏览器打开这个地址就能截图:
```
http://127.0.0.1:19387/api/inju-boot/boot.html?t=4.0&hud=0&telemetry=1 # 花体阶段
http://127.0.0.1:19387/api/inju-boot/boot.html?t=10.5&hud=0&telemetry=1 # 月球阶段
http://127.0.0.1:19387/api/inju-boot/boot.html?t=17.6&hud=0&telemetry=1 # 实体 LOGO
```
两张静帧就是这样抓的:headless Edge + CDP,等 90 帧后 `Page.captureScreenshot`,再转成 WebP(1920×1080,共 240 KB)。
**hero 动图**是 10 段拼起来的:动画本体暴露了确定性单帧驱动 `window.__render.set(t)`,
所以可以对每个阶段钉住播放头、各取 5 帧(段内连续、段间硬切),再交给 ffmpeg:
```powershell
ffmpeg -framerate 8 -i frames/f_%04d.png -vf "scale=720:-1:flags=lanczos,split[a][b];\
[a]palettegen=max_colors=64:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
-loop 0 docs/preview-stages.gif
```
三个踩过的坑,省得下次再走一遍:
1. **headless 下别加 `--disable-frame-rate-limit --disable-gpu-vsync`。** 那两个开关会让
`requestAnimationFrame` 的 dt 趋近 0,而 HUD 每 1/30 秒才重绘一次 —— 结果截到一张**没有 HUD** 的干净画面。
(用注入钩子数过 `fillText`:4 次 vs 去掉开关后的 10534 次。)
2. **GIF 对这个画面极其不友好。** 每颗粒子每帧都在动,等于每帧都是全帧更新。
实测 800×450 / 90 帧 是 **16 MB**;把动画自带的 `render.grain`(每像素胶片噪点)临时关掉后直接降到 7.5 MB。
粒子数与数字雨开关几乎不影响体积(5.1 MB vs 5.1 MB),真正决定体积的只有**分辨率 × 帧数**。
3. **ffmpeg 在这台机器上写不了 `%TEMP%`**(`Permission denied`),输出直接落仓库目录里就行。
</details>
---
## 快速开始
### 安装
**方式 A:交给 Harness 自己装(推荐)**
让 Harness 里的 Agent 调用 `plugin_manager`:
```
plugin_manager action=install_bundle target=<这个仓库的绝对路径>
```
它会替你写 profile 的 `package.json` / `cordis.patch.yml` 并完成安装 —— **不要手改那两个文件**。
**方式 B:dsh CLI**
```powershell
dsh --profile <你的 profile> plugin add "file:<这个仓库的绝对路径>"
# 或直接从 GitHub 装:
dsh --profile <你的 profile> plugin add "github:inju0524/dsh-inju-boot"
```
装完**重启一次 Harness**(新增 bundle 需要重新组装 profile)。下一次启动就会看到动画。
> 卸载:`plugin_manager action=remove_bundle target=dsh-inju-boot`
> 只是不想看了:设置页关掉「启用启动动画」,或在 `cordis.patch.yml` 里给这一行加 `disabled: true`。
### 动画从哪来
插件按这个顺序找动画:
1. **设置页里显式填的「动画目录」**(填到含 `inju-particles.html` 的目录)
2. **插件的上一级目录** —— 把这个插件文件夹放进你自己的动画项目里,它就用那份**活的**动画,
你在 `inju.config.js` 里改一个数字,刷新页面就生效
3. **插件自带的 `assets/` 副本** —— 独立 clone 出来时用这份,保证任何机器上都能播
---
## 设置项
设置 → **INJU 启动动画**。
| 分组 | 项 | 说明 |
|---|---|---|
| 播放 | 启用启动动画 | 总开关 |
| | 每次启动只播一次 | 会话级去重,开发时热重载不会反复重播 |
| | 时长倍率 | 等比拉伸整条时间轴(>1 更慢) |
| | 结束阶段 | 播到第几个阶段末尾就淡出(默认播完整个循环) |
| | 淡出时长 | 结束时的淡出毫秒数 |
| 背景音乐 | 背景音乐 / 音效 | **是否播放背景音乐**。关掉时连引擎都不加载(`?audio=0`) |
| | 背景音乐音量 | 0~1,覆盖动画配置里的 `audio.master` |
| | 首次点击只解锁声音 | 默认开。第一次点击/按键只用来出声,**不中断动画** |
| | 自动播放被拦下时提示 | 只在浏览器确实拦下自动播放时才出现 |
| 跳过 | 点击中断动画 | 是否允许**点击**中断动画 |
| | 按键中断动画 | 是否允许**按键**中断动画(Esc 始终可跳过) |
| | 显示跳过按钮 / 按钮文字 | 右下角的跳过按钮 |
| 画面 | LOGO / 虚影文字 | 虚影文字与最终实体 LOGO 都用它 |
| | 粒子数量 | 1000 ~ 400000 |
| | 粒子尺寸 / 粒子亮度 / 泛光强度 / 机距 | 直通动画的 `size` / `intensity` / `bloom` / `dist` |
| | 极客 HUD / 终端层 | **可选,默认开启**。DSH 铭牌与启动命令行 + 数字雨 + 目标框 + 扫描线 + 点云小图 + 终端日志 + 遥测 + hex dump |
| | 模式 | `flower3d` 程序化 3D 雏菊 / `photo` 照片粒子化 |
| 窗口顶栏 | 顶栏融入动画 | **默认开启**。仅 Windows 桌面端:让最小化/最大化/关闭背后的标题栏背景变透明 |
| | 隐藏「应用 / 编辑」菜单 | **默认开启**。仅 Windows 桌面端:动画期间隐藏 Desktop 注入的菜单条,播完恢复 |
| 高级 | 动画目录 | 留空 = 自动探测 |
| | HUD 终端台词 | 一行一条 `秒 文本`;`{text}` 会换成真正的 LOGO 文字 |
| | `inju.config.js` 覆盖 | 一段 JSON,**深度合并**到动画目录那份 `inju.config.js` 之上 |
配置落在 `<DSH_HOME>/inju-boot/config.json`(默认 `~/.dsh/inju-boot/config.json`),
`cordis.patch.yml` 里那一行的 `config` 是出厂种子。三层优先级:
**内置默认值 ← YAML 种子 ← config.json(设置页写这里)**。
### 「`inju.config.js` 覆盖」能改什么
动画配置的**全部**字段,键名与 `inju.config.js` 完全一致:
```json
{
"camera": { "orbitYaw": 0.6, "dist": 20 },
"moon": { "radius": 2.6, "craters": 110 },
"flower": { "layers": [ { "n": 40, "len": 2.4 } ] },
"render": { "bloom": 1.2, "exposure": 1.1 },
"hud": { "brand": { "title": "MY HARNESS", "subtitle": "我的启动序列" } },
"stages": [ { "dur": 3.2 }, { "dur": 4.0 } ]
}
```
`stages` 数组**按下标合并**,只写想改的那几项就够了。
---
## 工作原理
### 一、动画托管:原文件一个字节都不改
`inju-particles.html` 里 `<script src="inju.config.js">` 是相对路径,所以当 Host 把动画挂在
`/api/inju-boot/boot.html` 时,浏览器会去取 `/api/inju-boot/inju.config.js` —— 而这条路由返回的是
**运行时合成**的配置:
```
动画目录的 inju.config.js ← 插件 advanced 覆盖 ← 时间轴倍率
```
Host 用 `node:vm` 在沙箱里取出动画目录那份配置对象(纯字面量,无副作用),深合并后序列化成
`window.INJU_CONFIG = {...}`。所以你既可以直接改项目里的 `inju.config.js`,也可以在设置页叠加覆盖,
互不打架。**动画本体和它的配置都是按请求现读的,改完刷新页面即生效,不用重启 Harness。**
Host 半身在 `ctx.inject(['webServer'], …)` 里注册 7 条 `exact` 路由,全部挂在 `ctx.effect` 上,卸载即回收:
| 路由 | 作用 |
|---|---|
| `GET/POST /api/inju-boot/config` | 读 / 写配置(`replace: true` 整份替换,`reset: true` 回出厂) |
| `GET /api/inju-boot/boot.html` | 动画本体(原文件原样吐出,供覆盖层 iframe 使用) |
| `GET /api/inju-boot/inju.config.js` | 运行时合成的 `window.INJU_CONFIG` |
| `GET /api/inju-boot/inju-audio.js` | 音效引擎 |
| `GET /api/inju-boot/inju-particles.html` | 原文件(想单独打开看的时候用) |
| `GET /api/inju-boot/asset?name=…` | 动画目录里的其它资源(白名单扩展名 + 拒绝任何跨目录/绝对路径) |
| `GET /api/inju-boot/preview-url` | 覆盖层 iframe 的查询串 |
### 二、背景音乐怎么做到自动播放
「必须点一下才有声音,但一点动画就没了」是**两个原因叠在一起**:
1. **动画的音频引擎不会自己启动。** `inju-audio.js` 的 `init()`(里面才 `new AudioContext()`)
**只从 `unlock()` 调用**,而 `unlock()` 只挂在动画自己窗口的首次 `pointerdown`/`keydown` 上。
所以哪怕浏览器策略再宽松,没人推它一把它也永远不出声。
2. **那一下点击同时又命中了跳过规则**,动画当场结束。
覆盖层的解法:
1. **主动解锁。** iframe 与宿主同源,覆盖层在 iframe 的 `load` 事件里直接调用
`iframe.contentWindow.INJU_AUDIO.unlock()`。DSH Desktop 是 Electron,默认
`autoplayPolicy = no-user-gesture-required`,所以**这一步之后不需要任何点击,音乐直接响**。
2. **第一次手势只解锁声音。** 普通浏览器会把 AudioContext 挂起,这时必须有一次真实手势。
开启「首次点击只解锁声音」后,**第一次**点击/按键被吃掉(只用来出声),动画继续播;
从**第二次**起才按「点击中断动画 / 按键中断动画」走。
3. **只在真的被拦下时才提示。** 覆盖层新建一个 AudioContext 探它是 `running` 还是 `suspended`
(自动播放策略按**顶层文档**判定,同源 iframe 与之共享),只有 `suspended` 才在底部显示
「点一下画面开启声音(这次点击不会中断动画)」。
4. 每次手势都会**再推一次** `unlock()`(幂等),所以引擎晚一点就绪也接得上。
### 三、Windows 顶栏:为什么它一开始会「压」在动画上
这不是调大 z-index 能解决的 —— 两处漏点各有各的原因:
**① 左上角「应用 / 编辑」菜单条(DOM)。**
Desktop 的 preload 往 `body` 上注入了一个 `div[data-windows-menu]`(内含 shadow root),
`position: fixed; z-index: 1100`。而 `shell.overlay` 的条目渲染在
`.BynINW_overlayLayer{position: absolute; z-index: 20}` 里 —— **那是一个层叠上下文**,
所以覆盖层写多大的 z-index 都只在 `20` 这一层里有效,必然被 `1100` 压住。
→ 解法:动画期间 `display: none` 掉它,播完移除样式自动恢复。
**② 右上角最小化 / 最大化 / 关闭(原生 Window Controls Overlay)。**
这三个按钮由 Chromium 画在网页内容**之上**,CSS 碰不到按钮本身。但它们的**背景色是可变的**:
preload 里有个探针元素用 `background-color: var(--dsw-specific-sidebar-fill)` 量出色值,
再经 `windowsAppearance` IPC 调 `mainWindow.setTitleBarOverlay()`。而主进程的校验是
`/^(?:#[\da-f]{3,8}|rgba?\([\d.,%\s]+\))$/` —— **8 位 hex 和 rgba 都接受**。
→ 解法:动画期间把 `--dsw-specific-sidebar-fill` 置为 `transparent`,原生顶栏背景就变成
`rgba(0, 0, 0, 0)`,动画从三个按钮后面透出来;DOM 里那条 40px 标题栏
(`.BynINW_frame::before`)用的是同一个变量,一起透明。顺带把 `--dsw-alias-label-primary`
(原生按钮的符号色)按**覆盖层底色**算成亮/暗,保证按钮在动画上看得清。
这个 `<style>` 必须挂在 **`document.head`**:preload 的 MutationObserver 只观察 head 的
childList/subtree,挂在别处不会触发它重新量色。它是本插件唯一一次「组件之外的 DOM 写入」,
所以每条退出路径(正常播完 / 超时 / 卡住 / 组件卸载)都会把它移除,不会留下一个透明顶栏。
### 四、覆盖层怎么知道「播完了」
它**不去猜时长**,而是读动画**自己的进度条** `#scrub` —— 那是动画公开的播放头
(`value = T/TOTAL*1000`)。总时长来自 Host 算好的 `resolved.stages`,所以「结束阶段」选到第几秒是精确的。
另外还有三层兜底:文档 8 秒没起来、进度一直停在 0 超过 20 秒、以及 `maxDurationMs` 硬上限 ——
任何意外都不会让覆盖层永久挡住界面。
> **为什么用 iframe**:动画本身是一支自带全局 CSS(`html,body{overflow:hidden}`)和 WebGL2
> 上下文的独立作品,直接塞进 React 树会污染宿主页面的全局样式。iframe 把它隔离成一个独立
> artwork 文档。设置页则是纯 React + `--dsw-alias-*` 主题令牌,与宿主其它设置页同构。
---
## 自检
**零依赖,不需要 Harness、不需要浏览器**(CI 也跑这三套):
```powershell
npm test # 三套一起跑
node tools/selftest.mjs # Host 半身:84 项
node tools/selftest-client.mjs # 浏览器半身:62 项
node tools/check-animation.mjs # 动画本体:38 项
node tools/check-animation.mjs --dump --w=1440 --h=900 --t=9.5 # 打出 HUD 排版
```
| 套件 | 验什么 |
|---|---|
| `selftest.mjs` | 起一个真 http 服务跑 7 条路由:配置读 / 写 / 整份替换 / 复位、路径穿越防护、越界输入收敛、动画目录解析、`inju.config.js` 合成、身份层下发 |
| `selftest-client.mjs` | 把 `client.js` 当真正的 bundle 加载:ModuleLoader 契约、插槽注册、音频桥、首次手势策略、顶栏处理、源码卫生(只允许主题令牌、不 import 宿主 Client 包、不往 body 追加节点) |
| `check-animation.mjs` | 内联脚本语法;**把 `hudDraw()` 抽出来配一个会记账的假 2D 上下文真跑一遍**,断言铭牌/命令行/遥测字段/进度条产品名/DSH 日志真的被画出来、标题居中且带字距、1920/1280/640 三种宽度下都不越界 |
**在线(对着正在运行的 Harness 打真实请求)**:
```powershell
node tools/selftest-live.mjs # 默认 http://127.0.0.1:19387
node tools/selftest-live.mjs http://127.0.0.1:19387
```
40 项:Host 路由在线、动画本体与音效可被浏览器取到、**服务出去的 `inju.config.js` 能真的跑出
`window.INJU_CONFIG` 且合并正确**、极客 HUD 与音频开关都真的接通了、bundle 解析链、profile 的 bundles 列表。
`tools/` 里还有三个**只读**的 asar 工具,用来翻 DSH 自带文档与源码(这个插件的顶栏那一段就是靠它们查出来的):
```powershell
node tools/asar-list.mjs "<...>/resources/app.asar" windows-titlebar
node tools/asar-extract.mjs "<...>/resources/app.asar" "lib/preload-app.cjs" ./out
node tools/asar-grep.mjs "<...>/resources/app.asar" "app-region" --ext .js
```
---
## 排障
| 现象 | 原因 / 处理 |
|---|---|
| 启动没动画 | 设置页看「当前生效」那一行:`✗` 表示没找到动画目录,把「动画目录」填成含 `inju-particles.html` 的目录 |
| 设置页打不开 / 列表里没有入口 | Host 半身没挂上:确认 profile 的 `dsh.profile.bundles` 里有 `dsh-inju-boot`,然后重启 Harness |
| 画面干净但**没有极客 HUD** | 「极客 HUD / 终端层」被关掉了(默认开),打开它 |
| 想开 HUD 但看不到台词 | 台词在「HUD 终端台词」里,逐行 `秒 文本`;清空就不显示日志 |
| **完全没有声音** | ①「背景音乐」是不是被关了;② 音量是不是拉到 0;③ 普通浏览器下需要**先有一次点击**(第一次点击只解锁声音、不会中断动画,覆盖层会在底部提示你);④ 桌面端正常应自动出声 |
| **点一下就中断了动画** | 关掉「点击中断动画」;若想保留它又要点一下出声,确认「首次点击只解锁声音」是**开**的 |
| 左上角「应用 / 编辑」压在动画上 | 开「隐藏「应用 / 编辑」菜单」 |
| 右上角三个按钮背后有一条色带 | 开「顶栏融入动画」。若无效,可能是自定义主题绕过了 `--dsw-specific-sidebar-fill` |
| 动画结束后顶栏颜色没还原 | 不该发生;插件在播完 / 超时 / 卸载时都会移除那个 `<style>`。真遇到刷新页面即可 |
| 动画在播但界面被挡住很久 | 把「结束阶段」调到 09,或把「时长倍率」调小;Esc / 点击 / 任意键都能跳过 |
| 想把铭牌换成自己的 | 「`inju.config.js` 覆盖」里写 `{"hud":{"brand":{"title":"…","subtitle":"…"},"shell":"…"}}` |
| 想改回出厂 | 设置页「恢复出厂默认」,或删掉 `<DSH_HOME>/inju-boot/config.json` |
---
## 目录
```
dsh-inju-boot/
├── package.json # 包清单:dsh.bundle.patch + dsh.client(platform=web, immediately)
├── cordis.patch.yml # profile 层:insert 一行把 Host 半身挂上去(config 是出厂种子)
├── index.js # Host 半身:配置 + /api/inju-boot/* 路由(零依赖,只用 node: 内建模块)
├── client.js # 浏览器半身:启动覆盖层(音频桥 / 顶栏处理)+ 设置页(手写 ModuleLoader bundle)
├── icon.svg # 插件图标
├── locale/{zh,en}.json # 展示文案
├── docs/ # README 的 hero 动图(10 阶段蒙太奇)+ 两张静帧
├── assets/ # 动画副本(独立 clone 时的兜底;放在动画项目里则走项目那份)
├── tools/ # 三套自检 + 三个只读 asar 工具
└── .github/workflows/ # CI:push / PR 自动跑三套自检
```
### 依赖与兼容
- **零运行时依赖**:Host 半身只用 `node:` 内建模块;浏览器半身只用 shell 已经提供的 `react`。
不 import 任何 `@deepseek-ai/*` 运行时包 —— 那一层 API 面会漂移,能不依赖就不依赖。
- 插件挂载用 `ctx.inject(['webServer'], …)`,插槽用 `ctx.slots.inject(...)`(等插槽出现才注册),
所以宿主缺这些能力时是**安静地不激活**,而不是启动报错。
- 顶栏那两项只在 **Windows 桌面端**有效(靠 preload 打的 `data-windows-titlebar` 标记);
macOS 的窗口本来就透明,浏览器里根本没有这些元素,开了也是空操作。
### 验证边界(说实话)
三套离线自检 + 一套在线自检共 **224 项断言**,覆盖的是**契约与绘制调用**,不是截图。
动画那一套会把真正的 `hudDraw()` 抽出来、配一个会记账的假 2D 上下文跑一遍,
断言每个元素真的被画了出来、位置与居中对不对、窄窗口会不会越界 —— 这是没有浏览器时能拿到的最强证据。
但字体的真实字宽与假上下文的估算(0.6em)可能有几个百分点出入,
所以 640px 那种极限宽度下「缩字号 vs 不画」的边界可能略有偏移(两侧预留是按实测宽度算的,不会重叠)。
---
## 素材与许可
- 代码:[MIT](LICENSE) © inju0524
- 动画本体(`inju-particles.html` / `inju-audio.js` / `inju.config.js`)与内嵌照片(`assets/flower.jpg`)
为本项目自有素材,随本仓库一并以 MIT 分发;照片请勿单独转载。
---
## English
**dsh-inju-boot** is a DeepSeek Harness plugin that plays a full-screen WebGL2 particle startup
animation every time Harness launches — a procedural 3D daisy blooms, collapses into a particle moon,
resolves into ghost text and snaps into a solid logo, with a geek HUD/terminal layer (digital rain,
target lock box, scanline, live point cloud, terminal log, telemetry, hex dump) and a programmatically
synthesised Apple-keynote-style soundtrack. The hero GIF above walks through all ten stages in 6.3 s.
- **Zero dependencies.** The Host half uses only `node:` built-ins; the browser half only uses the
`react` the shell already provides. No `@deepseek-ai/*` runtime imports.
- **Everything is configurable** in Settings → *INJU 启动动画*: text, particle count, size, bloom,
brightness, camera distance, playback speed, end stage, fade-out, skip behaviour, background music
and volume, HUD on/off, the DSH nameplate / terminal lines, and a free-form deep-merge override of
the animation's own `inju.config.js`.
- **The animation file is never modified.** The Host composes `window.INJU_CONFIG` at request time by
merging the animation project's own config with your overrides.
- **Background music autoplays** on the desktop app: the overlay proactively calls the animation's
`INJU_AUDIO.unlock()` (the engine only builds its `AudioContext` there). The *first* click/keypress is
consumed to unlock audio and no longer dismisses the animation.
- **The Windows titlebar blends into the animation.** The native min/max/close background colour is
driven by `--dsw-specific-sidebar-fill` (the preload probes it and pushes it over IPC to
`setTitleBarOverlay`), so the plugin sets it to `transparent` while playing; the injected
*Application / Edit* menu bar (`div[data-windows-menu]`, `z-index: 1100`) is hidden for the duration.
```powershell
# install (let the Harness agent do it):
plugin_manager action=install_bundle target=<absolute path to this repo>
# or with the CLI:
dsh --profile <profile> plugin add "github:inju0524/dsh-inju-boot"
```
Then restart Harness. Tests need neither Harness nor a browser: `npm test` runs 184 assertions across
three suites, and `node tools/check-animation.mjs --dump` prints the real HUD draw calls as a layout map.
Install
dsh plugin --profile web add github:inju0524/dsh-inju-boot
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-inju-boot from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.