dsh-boot-suite 2.13.21 → 2.15.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +9 -2
  3. package/lib/client.js +445 -23
  4. package/lib/host-startup.js +196 -3
  5. package/lib/host-wallpaper.js +32 -11
  6. package/lib/splash.js +270 -0
  7. package/package.json +2 -2
  8. package/templates/division-bigfish-codex/app.js +255 -0
  9. package/templates/division-bigfish-codex/assets/egg-bg.png +0 -0
  10. package/templates/division-bigfish-codex/assets/voice-full-plus.wav +0 -0
  11. package/templates/division-bigfish-codex/assets/voice-line1.wav +0 -0
  12. package/templates/division-bigfish-codex/assets/voice-line2.wav +0 -0
  13. package/templates/division-bigfish-codex/assets/voice-line3.wav +0 -0
  14. package/templates/division-bigfish-codex/assets/voice-line4.wav +0 -0
  15. package/templates/division-bigfish-codex/assets/voice-orig1.wav +0 -0
  16. package/templates/division-bigfish-codex/assets/voice-orig3.wav +0 -0
  17. package/templates/division-bigfish-codex/assets/voice-reconnect.wav +0 -0
  18. package/templates/division-bigfish-codex/assets/whale-badge-line.png +0 -0
  19. package/templates/division-bigfish-codex/config.js +24 -0
  20. package/templates/division-bigfish-codex/index.html +1 -0
  21. package/templates/division-bigfish-codex/motion.js +30 -0
  22. package/templates/division-bigfish-codex/preview-easteregg.mp4 +0 -0
  23. package/templates/division-bigfish-codex/preview-normal.mp4 +0 -0
  24. package/templates/division-bigfish-codex/render.html +1 -0
  25. package/templates/division-bigfish-codex/style.css +18 -0
  26. package/templates/division-bigfish-codex/template.json +10 -0
  27. package/templates/division-bigfish-codex/voice-env.js +1 -0
  28. package/templates/division-bigfish-codex/voice.js +10 -0
  29. package/templates/division-bigfish-codex//344/272/244/346/216/245-Codex/347/211/210/346/216/245/345/205/245/346/226/271/346/241/210.md +207 -0
  30. package/templates/division-bigfish-codex//346/217/222/344/273/266/346/216/245/345/205/245/345/245/221/347/272/246.md +177 -0
  31. package/test/smoke-client.mjs +4 -4
  32. package/test/smoke-host.mjs +23 -1
  33. package/test/smoke-package.mjs +89 -1
  34. package/test/smoke-splash.mjs +2 -0
  35. package/test/smoke-wallpaper-client.mjs +1 -1
  36. package/test/smoke-wallpaper-host.mjs +7 -1
  37. package/tools/README.md +1 -1
  38. package/tools/install.ps1 +7 -4
  39. package/tools/make-upgrade-helper.mjs +20 -0
  40. package/tools/stage-local.mjs +158 -0
@@ -0,0 +1,207 @@
1
+ # 全境大肥鱼 · Codex 版采用与接入交接
2
+
3
+ > 2026-10-31 · Lead 归档。用户决定:**以 Codex 版(division-editable)为基底**,替代我们此前的 p6 融合版。
4
+ > 本目录 `01-源码/docs/proto/division-codex/` 是从 Codex 原始目录复制并做「可渲染 + 参数化」适配后的工作副本;**原仓库既有文件未改**。
5
+
6
+ ## 1. 文件清单
7
+
8
+ | 文件 | 说明 |
9
+ |---|---|
10
+ | `index.html` / `app.js` / `motion.js` / `voice.js` / `style.css` | Codex 原版(未改结构) |
11
+ | `config.js` | 已改为我们的默认值:`username='人类与AI的日常'`、tip1 文案、`duration 25.92`、`loadingStart 9.2`、进度曲线按参考实测重排(63% 保持到 25.4s、25.45 跳 100%) |
12
+ | `render.html` | 渲染用副本:隐藏右侧编辑侧栏、`?t=<秒>` 定格帧(通过 `#seek` input 事件驱动 `seek()`) |
13
+ | `render_frames.py` | Chrome headless 逐帧渲染 → `division-codex-1080p30.mp4`;3 线程、独立 user-data-dir、失败重试 |
14
+ | `division-codex-1080p30-audio.mp4` | 成片:1920×1080 / 30fps / 25.92s / 2.06MB,混入原片环境声(`division-code/render/src-audio.m4a`) |
15
+ | `still-2/5/12/16/25.png` | 成片抽帧 |
16
+ | `compare-ref-vs-codex.png` | 参考(左)vs Codex 版(右)t=2/5/12/16/25 对比 |
17
+
18
+ ## 2. 架构与参数
19
+
20
+ 纯 **SVG + JS** 参数化动画(无视频、无位图、无逐帧数据);全部参数集中在 `config.js`:
21
+
22
+ ```js
23
+ window.CONFIG = {
24
+ username, title, description, // 可编辑文字
25
+ duration: 25.92, loadingStart: 9.2, // 时间轴
26
+ progress: [[9.2,0],[10,10],...,[25.45,100],[25.92,100]], // 进度曲线
27
+ voiceY, voiceWidth, // ISAC 波形条位置/宽
28
+ }
29
+ ```
30
+
31
+ - 动画状态机在 `motion.js`(`HUDMotion.state(t)`):展开/退场/重新展开/脉冲/呼吸/闪入,纯解析函数,可任意 seek。
32
+ - 进度弧、百分比、状态文字、tips 面板、ISAC 波形均由 `app.js` 每帧重绘;`render.html` 通过 `#seek` 控制时间。
33
+ - 右侧 `aside` 是 Codex 自带的可编辑侧栏(username/heading/body、播放、跳转、导入语音、保存 JSON、恢复默认);渲染版已隐藏,**功能保留给插件侧移植**。
34
+
35
+ ## 3. 接入插件(给插件维护上下文)
36
+
37
+ 引擎(splash.js)当前只支持 `steps` 与 `layers` 两种模板,Codex 版是独立 HTML/SVG 应用,建议新增**第三种模板类型 `html`**:
38
+
39
+ - 模板目录:`%DSH_HOME%\startup-templates\bigfish-division-html\`(或打包进插件 `templates/`),含 `index.html` + `config.js` + `app.js` + `motion.js` + `voice.js` + `style.css`。
40
+ - 播放:在启动层内用 `<iframe>`(或 ShadowRoot)加载 `template-asset/bigfish-division-html/index.html?username=<encodeURIComponent(username)>`;iframe 内读 query 合成 `CONFIG`,或宿主 `postMessage({type:'config',...})`。
41
+ - 跳过:宿主顶部显示「跳过 ▸」,点击/ESC 时 `iframe.contentWindow.postMessage({type:'skip'})`;动画 25.92s 结束 `postMessage({type:'done'})` 由宿主收尾。
42
+ - 设置面板:仿档案暖白专属组,仅在模板选中时显示 `连接用户名`(及未来 tips 编辑、语音导入);默认值来自 `config.js`。
43
+ - 音频:可直接复用 `audio.m4a`(原片环境声)作为模板资产,或允许用户导入。
44
+
45
+ **参数契约(最小集)**:`username`、`title`、`description`(tips 第 1 条)、`duration`(默认 25.92)、可选 `voice`(音频文件 + 开始时间)。
46
+
47
+ ## 4. 与参考的差距(诚实说明)
48
+
49
+ `compare-ref-vs-codex.png` 左=参考 V3(无鼠标),右=Codex 版:
50
+
51
+ 1. 环几何为几何重建:参考亮弧带 r≈50–62(stage 1280×720),Codex 更内收、外层刻度更细;无 bloom 光晕层。
52
+ 2. 字体:参考是横向压缩 DIN 类;Codex 用系统字体,中文更宽。
53
+ 3. 百分比/状态文字位置与字号略不同;tips 面板行距/字距为近似。
54
+ 4. ISAC 波形条:参考原片没有,是 Codex 新增(用户要求保留)。
55
+ 5. 无鼠标移动(已按用户要求)。
56
+
57
+ 用户评估:约 85% 接近目标。后续打磨方向:字体横向压缩、环几何/亮度权重、面板排版细化;插件接入 = 交给插件上下文。
58
+
59
+ ## 5. 复现命令
60
+
61
+ ```powershell
62
+ $py = 'C:\Users\Admin\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe'
63
+ & $py 'F:\Lee的GitHub仓库\DeepSeek harness 开机动画\01-源码\docs\proto\division-codex\render_frames.py'
64
+ # 输出 division-codex-1080p30.mp4,再与 src-audio.m4a 混流(见上)
65
+ ```
66
+
67
+ 单帧:`render.html?t=12.34`(Chrome headless `--window-size=1920,1080 --screenshot`)。
68
+
69
+ ## 6. v3 调整(2026-10-31 晚)
70
+
71
+ - `config.js` 新增 `startAt:3`:动画从源时间 3.0s 起播,有效时长 **22.92s**(25.92−3)。
72
+ - `app.js`:初始 `t=startAt`;播放结束/重播/连接按钮都回 `startAt`;seek 下限 `startAt`;时间显示按有效时长;**「连接中」始终显示为「连接中:<username>」**(不再在 3.05s 后切成纯「连接中」)。
73
+ - 成片 `division-codex-1080p30-audio.mp4` 已按 3.0→25.92 重渲并裁掉音轨前 3s(22.92s)。注意:源片 3.0–4.3s 本身是转场黑场,所以成片开头约 1s 黑屏属于原有转场,不是丢帧。
74
+ - 插件侧接入时:iframe 加载后 `duration` 用 22.92,进度/事件时间仍按源时间(绝对),或把 `startAt` 传给页面。
75
+
76
+ ## 7. v7 新增参数与行为(2026-11-01 更新)
77
+ - `startAt:3`:跳过源片前 3 秒(源 3.0–4.3s 本身是黑场转场,故开头约 1.3s 黑属正常)。
78
+ - `speedStart:9.2, speed:1.5, speedEnd:25.45`:加载段 1.5 倍速(页面时钟按 dt 增量、仅该区间乘 1.5;音频在成片里用 atempo 同步,页面用 syncAudio 跟随 t)。
79
+ - 进度条从 0% 起:`progress` 曲线首段为 `[9.2,0],[10,0],[10.9,0],[11.5,10]`,保证加载界面淡入后先显示 0% 再走。
80
+ - 右上角「跳过 ▸」按钮:`style.css` 中 `#skip` 系列样式 + `app.js` 末尾注入;点击/Esc 触发 `dsh-startup-skip` 事件、`parent.postMessage({type:'dsh-startup-skip'})`、`window.__DSH_SKIP__()`。插件宿主要接这三者之一(建议 postMessage)。
81
+ - 成片时长 17.5s(= 6.2s 连接/黑场 + 10.833s 加载@1.5x + 0.467s 尾部停留);mp4 内跳过按钮为画面固定元素,交互页里才可点击。
82
+
83
+ ## 8. v8 新增:掉线彩蛋 + 首帧修正(2026-11-01)
84
+
85
+ ### 8.1 首帧「闪一下」修正
86
+ - `config.js` 的 `startAt` 由 3.0 改为 **3.4**(落在源片 3.05–4.30 的黑场里),视频第一帧即纯黑;`render_frames.py` 同步 `S0=3.4`。
87
+
88
+ ### 8.2 彩蛋触发
89
+ - 参数(`config.js` 的 `egg`):`{chance:0.28, at:9.2}`;`chance` 为每次启动的触发概率,`at` 为判定时间点(源片时间,即加载阶段开始)。
90
+ - 逻辑(`app.js` 末尾 `掉线彩蛋` IIFE):动画播放到 `t>=egg.at` 时掷一次 `Math.random()<chance`,命中则**硬切**到掉线画面;每次启动**只判定一次**。`?egg=1` 可强制触发(用于测试/录制)。
91
+ - 硬切:暂停播放与音频、隐藏「跳过」按钮(`body.egg-on #skip{display:none}`)。
92
+
93
+ ### 8.3 掉线画面
94
+ - 结构:`assets/egg-bg.png` 底图 → `#egg-veil` 半透明白纱(`rgba(232,238,246,.42)` + `backdrop-filter: blur(3px)`)→ `#egg-card` 错误对话框。
95
+ - 对话框按用户截图重建为 HTML/CSS(便于按钮可点、文字可改):橙色「!」+「错误」、《The Division®》服务目前无法使用,请稍后再试。、红色 `DELTA` / `C-0-1302` 条、按钮「重新连接」。
96
+ - 文案/错误码在 `index.html` 的 `#egg-card` 内,可直接改;如需换成用户原始截图作为纯图层,可把 `#egg-card` 的背景图换成 PNG(但会失去按钮交互)。
97
+
98
+ ### 8.4 重新连接与结束
99
+ - 点击「重新连接」:关闭彩蛋 → `seek(loadingStart)` → `playing=true`,**重播进度条 0%→100%**。
100
+ - 到达结尾(`t>=duration-0.02`):`flashDone()` 白闪 260ms,然后依次触发:
101
+ 1. `document` 事件 `dsh-startup-done`
102
+ 2. `parent.postMessage({type:'dsh-startup-done'}, '*')`(iframe 场景)
103
+ 3. `window.__DSH_DONE__()`(若宿主提供)
104
+ - 宿主(插件)应在收到 `dsh-startup-done` 后关闭启动画面、进入主界面;「跳过」保持原有 `dsh-startup-skip` 三种握手。
105
+
106
+ ### 8.5 暴露的调试接口
107
+ - `window.__DSH_APP__ = {play,pause,seek,showEgg,flashDone}`;`?egg=1` 强制彩蛋、`?play` 自动播放、`?showegg` 直接展示彩蛋画面(截图/录制用)。
108
+
109
+ ### 8.6 v8 成片
110
+ - `division-codex-v8-normal.mp4`:17.4667s,正常路径(末尾 0.35s 白闪)。
111
+ - `division-codex-v8-easteregg-demo.mp4`:19.6667s,演示彩蛋路径(5.8s 连接 → 2.2s 蛋画面 → 11.3s 重播加载 → 0.35s 白闪);随机性无法在成片里体现,仅作视觉演示。
112
+ - 音频待用户重做,两版均为无声。
113
+
114
+ ## 9. v9 修改(2026-11-01):文案 / 黑纱 / 固定概率 / 配音
115
+
116
+ ### 9.1 文案与纱
117
+ - 错误文案:《The Division®》… → **《战术大肥鱼®》服务目前无法使用,请点击重试按钮。**
118
+ - 纱:白纱 → **黑纱**(`rgba(6,8,12,.55)` + `backdrop-filter: blur(3px) saturate(.85)`),底图与对话框不变。
119
+ - 「跳过」在彩蛋期间仍隐藏(`body.egg-on #skip{display:none!important}`)。
120
+
121
+ ### 9.2 触发概率
122
+ - 固定 **28%**(`app.js` 顶部 `var EGG_CHANCE=0.28`),用户不可调,也不走 `config.js`。
123
+ - 调试:URL 加 `?egg=1` 强制触发、`?mute=1` 静音(含配音)。
124
+
125
+ ### 9.3 配音
126
+ - 源:`C:\Users\Admin\Downloads\Music\全境封锁主题-配音-完整-男-V2.wav`(8.722s,3 句)。
127
+ - 切句(已生成):`assets/voice-line1.wav`(2.650s,第 1 句)、`assets/voice-line3.wav`(2.772s,第 3 句);中间的【身份确认】跳过。
128
+ - 播放规则(`app.js` 彩蛋 IIFE):
129
+ - 第 1 句:源时间 `voice.line1At=4.6s`(「连接中:用户名」显示后)播放一次;
130
+ - 第 3 句:源时间 `voice.line3At=25.45s`(进度 100%)播放一次;
131
+ - 彩蛋硬切(`showEgg`)→ `stopVoice()` 立刻 `pause()` 并 `currentTime=0`,配音中断;
132
+ - 正常结束:到 `duration-0.02` 后若第 3 句还没播完,最多等 **3500ms**,播完(或超时)才白闪并 `doneEmit`;
133
+ - 重播(点「重新连接」)只重置第 3 句,第 1 句不重播。
134
+ - 时间参数均可改:`config.js` 的 `voice.line1At/line3At`。
135
+
136
+ ### 9.4 成片(含配音,静音底混音)
137
+ - `division-codex-v9-normal.mp4`:**20.1667s**(17.1s 动画 + 2.7s 100% 停留等台词 + 0.35s 白闪);
138
+ - `division-codex-v9-easteregg-demo.mp4`:**22.3667s**(5.8s 连接 + 2.2s 彩蛋 + 11.3s 重播 + 2.7s 停留 + 0.35s 白闪);
139
+ - 配音落点:第 1 句 in 1.2s;第 3 句 normal in 17.033s、demo in 18.833s。
140
+
141
+ ## 10. v10:四句英文配音 / 波形动画 / ISAC→DSH / 最终时间轴(2026-11-01)
142
+
143
+ - 配音 4 句(`assets/voice-line1..4.wav`,来自 `全境封锁主题-配音-完整-男-补充.wav`):line1 3.375s、line2 3.232s、line3 3.300s、line4 1.911s(line4 = "Alert. Agent offline.")。
144
+ - 成片时间轴:line1 `1.20s`、line2 `6.60s`、line3 `16.63s`(100% 时)、彩蛋 line4 `硬切后 +1.0s`;音频轨已生成为 `audio-normal.m4a`(20.283s)与 `audio-demo.m4a`(22.483s)。
145
+ - 波形动画:`voice-env.js`(4 句 8ms RMS envelope,条数 422/404/413/239);`app.js` 按 `window.VOICE_SCHED` 命中时用 envelope 驱动 20 根波形条,说话才出现、说完消失;高度公式 `4+min(440, amp^0.8*440)`,`cfg.voiceWidth` 改 520 放大整组。
146
+ - ISAC→**DSH**:`voice.js` 的字母路径替换为 `<text>DSH</text>`(字号 178、字距 18、fill #e8e5d9),保留左侧小方块(四角方块)与波形;彩蛋文案统一为「《战术大肥鱼®》服务目前无法使用,请点击重试按钮。」。
147
+ - 彩蛋:概率固定 `EGG_CHANCE=0.28`(不可调,`?egg=1` 强制);硬切时立即 `stopVoice()` 静音;点「重新连接」重播进度条 0→100%;结束等 line3 播完(≤3500ms)再白闪 260ms → `dsh-startup-done` / postMessage / `__DSH_DONE__`。
148
+ - 成片:`division-codex-v10-normal.mp4`(20.283s,1920×1080/30fps)、`division-codex-v10-demo.mp4`(22.483s,含彩蛋路径演示);静默底片 `division-codex-v10-silent.mp4`。
149
+
150
+
151
+ ## 11. v11:恢复原始两句 + 彩蛋硬性时序 + 蛋内 DSH 波形(2026-11-01)
152
+
153
+ ### 11.1 恢复「最开始的第一句与第三句」
154
+ - 旧文件在 v10 被覆盖,现从 `v9-normal-audio.m4a` 精切恢复(不是从母版猜):
155
+ - `assets/voice-orig1.wav`:v9 音频 1.20–3.85s(2.65s)
156
+ - `assets/voice-orig3.wav`:v9 音频 17.03–19.80s(2.77s)
157
+ - 总时间轴改为 5 句(页面时间,秒):
158
+ `[4.6 orig1 2.65] [8.0 line1 3.375] [12.8 line2 3.232] [17.9 line3 3.300] [25.45 orig3 2.77]`
159
+ - 对应成片时间(video):orig1 1.2、line1 4.6、line2 8.2、line3 11.6、orig3 16.633。
160
+
161
+ ### 11.2 彩蛋硬性时序
162
+ - 掉线画面出现后:**静默 2s** → 播 `voice-line4`("Alert. Agent offline.",1.911s,同时 DSH 波形)→ 说完再**静默 2s** → 自动重播进度条 0%→100%。
163
+ - 参数:`voice.eggLineAt:2.0`、`voice.eggReplayWait:2.0`、`voice.line4Dur:1.911`;彩蛋总时长 2+1.911+2=5.911s。
164
+ - 「重新连接」按钮保留:点击立即重播(不等)。彩蛋期间隐藏「跳过」。
165
+
166
+ ### 11.3 蛋内 DSH
167
+ - `ensureEggVoice()` 在 `#egg` 内动态建 SVG(与主画面同 `voiceWidth/voiceY` 变换),`eggVoice(vt)` 用 `VOICE_ENV.line4` 的 8ms 包络做历史采样波形;只在 line4 期间显示。
168
+ - 「ISAC」标签已在 `voice.js` 改为 `<text>DSH</text>`(字号 178、字距 18)。
169
+
170
+ ### 11.4 文件与渲染
171
+ - `index.html` / `render.html` 重建为 UTF-8(原文件 GBK 损坏不可恢复),中文正常;`render.html` 支持 `?t=秒`、`?egg=1&eggt=秒`、`?play`。
172
+ - 帧渲染:normal 513 帧(S0=3.4、V1=5.8、V2=16.6333、VD=17.1);egg 177 帧(5.911s)。
173
+ - 成片:normal 20.283s(head + loading + hold 3.3 + 白 0.35);demo 26.183s(head 5.8 + egg 5.911 + 重播 10.833 + hold 3.3 + 白 0.35)。
174
+ - 音频:`audio-normal-v11.m4a`(20.283s)、`audio-demo-v11.m4a`(26.194s);demo 中 line1 在硬切处截断为 1.2s。
175
+
176
+ ### 11.5 已知取舍
177
+ - 音频无 BGM(用户后续另配)。
178
+ - 蛋画面 DSH/波形位置沿用主画面 voice 组参数;若想更大可调 `config.voiceWidth`(现 520)。
179
+
180
+ ### 11.6 v11 成片核对(2026-11-01)
181
+ - `division-codex-v11-normal.mp4`:1920×1080/30fps、**20.283s**、1,028,042B;抽帧 v11n-1.5/5.5/17.0/19.9 通过(连接态 orig1 波形、line1 波形、100% 处 orig3 波形、末尾白闪)。
182
+ - `division-codex-v11-demo.mp4`:1920×1080/30fps、**26.183s**、1,264,796B;抽帧 v11d-6.5/9.0/11.8/25.9 通过(蛋画面无波形→line4 波形出现→replay 0%→末尾白闪)。
183
+ - 已知可调:normal 波形整体偏细(高度系数 `4+min(440,pow(amp,.8)*440)`,如要更明显可把 440 提到 560–600 重渲);蛋内 DSH 位置/大小沿用主画面 voice 参数(`voiceWidth 520 / voiceY 908`)。
184
+
185
+ ### 12. v12 波形修复(2026-11-01)
186
+ - 波形:普通版原先 20 根柱等高(`4+min(440,amp^0.8*440)`),现改为逐柱历史采样 `st=max(0,vt-(19-k)*0.055)`、`h=4+min(360,amp^0.9*360)`;`VOICE_ENV` 重新按 p95 归一生成 7 组包络(orig1/line1/line2/line3/orig3/line4/reconnect,8ms RMS)。
187
+ - DSH 行:opacity 不再乘提示条 `labelOpacity`,只在说话窗口内显示;100% 后的静止段改为 99 帧 hold 段(`?hold=` 推进 voice 时间),第五句波形继续推进,不再冻屏。
188
+ - 掉线:延迟 2s→1s(`eggLineAt=1.0`);蛋时长 1+1.911+2=4.911s(148 帧);返回后清空蛋层波形;新增 `voice-reconnect.wav`(2.821s)在重播 0% 处播放,说完 +0.5s 接 line3,最后 orig3。
189
+ - 时间轴(成片时间):normal 20.334s——orig1 1.2 / line1 4.6 / line2 8.2 / line3 11.6 / orig3 16.633;demo 25.233s——orig1 1.2 / line1 4.6(截 1.2s) / line4 6.8 / reconnect 10.733 / line3 14.054 / orig3 21.567。
190
+ - 成片:`division-codex-v12-normal.mp4`(20.334s/1,078,114B)、`division-codex-v12-demo.mp4`(25.233s/1,236,673B),1920×1080/30fps,无声(BGM 待接)。
191
+ - 核对帧:v12n-1.5/5.5/19.0;v12d-7.5/11.2/13.8/14.3/21.6。
192
+
193
+ ### 13. v13 语音动画与音频对齐修复(2026-11-01)
194
+ - 根因:`VOICE_SCHED` 原为「页面时间」,加载段页面时间以 1.5× 推进,导致语音窗口被压短 1.5×;音频 wav 按视频时间 1:1 播放 → 话没说完 DSH/波形先结束(实测早退 0.7/1.06/1.1s);蛋层 `eggVoice` 不控制整行显隐 → line4 说完 DSH 仍停留到重播。
195
+ - 改法:`app.js` 引入视频时间 `VT`(`window.__VTIME__` 优先,否则由页面 t 反算),`VOICE_SCHED` 的页面 at 转为视频 at(`at<=9.2?at-3.4:5.8+(at-9.2)/1.5`),dur 保持音频秒;波形取 `VT-atV`。demo 用 `window.__VSCHED__`(视频时间)。`eggVoice` 在 `vt<0||vt>=line4Dur` 时隐藏整行。
196
+ - 渲染参数:`render_v12.py` 新增 `vtime=`(normal `i/30`、hold `16.6333+i/30`、replay `10.7333+i/30`、egg 不变)。
197
+ - 自检方法:`C:\Users\Admin\AppData\Local\Temp\divwork\audit.py`(裁剪 (760–1080,895–955) 最大灰度判 DSH 行 + 音频 8kHz/30fps RMS>0.01 判说话),要求 DSH 区间与音频区间误差 ≤0.1s。
198
+
199
+ - v13 复核(audit.py):normal DSH 区间 (1.2,3.87)/(4.6,8.0)/(8.23,11.43)/(11.6,14.9)/(16.73,19.5),对应语音结束 3.7/8.0/11.47/14.93/19.2,末段提前消失问题消除(末句留 0.3s 尾巴);demo 抽帧 6.2s 无 DSH、7.0s 有、10.0s(line4 说完)无;replay 14.07–17.37 为 line3 DSH,13.57 后 0.5s 空档无 DSH。
200
+ - 成片:normal 20.334s/1,086,513B、demo 25.233s/1,242,806B(1920×1080/30fps,无声)。
201
+
202
+ ### 13.1 v13.1 修复 100% 前 DSH 单帧闪烁(2026-11-01)
203
+ - 现象:normal 在 16.7s(100% 附近)DSH 行出现一帧「消失」。
204
+ - 根因:主段末帧 VT=16.6333 与 orig3 窗口起点 atV=16.63333(浮点)判定为「未开始」,而 100% 静止段首帧却按 16.6333 起算,造成 on→off→on。
205
+ - 修法:`app.js` 语音窗口比较加 20ms 容差 `VT>=s.at-0.02`;重渲 hold 段与边界帧后重编;逐帧复核 495–510 帧:100% 前(≤16.6s,64%)无 DSH,100% 帧起 DSH 持续显示,无 off 帧。
206
+ - 成片:`division-codex-v12-normal.mp4` 20.3337s/1,087,006B、`division-codex-v12-demo.mp4` 25.2333s/1,242,829B。
207
+ - 彩蛋时间:验收 demo 固定掉线点为可复现;真实交互页随机 0.28(每次启动只判定一次),`?egg=1` 可强制。
@@ -0,0 +1,177 @@
1
+ # 全境大肥鱼启动动画 · 插件接入契约(v13.1)
2
+
3
+ > 交接对象:负责 DSH 启动画面插件(boot-suite)的上下文。
4
+ > 模板包:`01-源码/templates/division-bigfish-codex/`(20 个文件,6.73 MB)
5
+ > 本文只描述「宿主 ↔ 模板」的接口与集成步骤;视觉细节见同目录 `交接-Codex版接入方案.md`。
6
+
7
+ ---
8
+
9
+ ## 1. 包内容
10
+
11
+ | 文件 | 作用 |
12
+ |---|---|
13
+ | `index.html` | **交互入口**(插件 iframe 加载它) |
14
+ | `app.js` | 主循环:时间轴、DSH 波形、彩蛋、跳过、done 事件 |
15
+ | `config.js` | **唯一需要改的配置**(标题/文案/用户名/时长/语音) |
16
+ | `motion.js` / `style.css` | 圆环与提示条动画、样式 |
17
+ | `voice.js` / `voice-env.js` | DSH 波形绘制与 7 组语音包络 |
18
+ | `assets/` | 7 句配音 wav + 掉线底图 `egg-bg.png` |
19
+ | `render.html` | 逐帧渲染/定格调试页(接入不需要) |
20
+ | `preview-normal.mp4` / `preview-easteregg.mp4` | 成片预览 |
21
+
22
+ **成片规格**:1920×1080 / 30fps / 无 BGM(BGM 由插件或后续补)。
23
+
24
+ | 版本 | 时长 | 说明 |
25
+ |---|---|---|
26
+ | 正常版 | 20.3337s | 无彩蛋 |
27
+ | 彩蛋演示 | 25.2333s | 掉线点固定在 5.8s,仅用于验收演示 |
28
+
29
+ ---
30
+
31
+ ## 2. 最小接入步骤
32
+
33
+ 1. 把整个 `division-bigfish-codex/` 目录拷进插件资源目录(相对路径必需保持)。
34
+ 2. 启动画面用 iframe 加载 **`index.html`**:
35
+
36
+ ```html
37
+ <iframe src="division-bigfish-codex/index.html"
38
+ style="width:100%;height:100%;border:0;background:#000"
39
+ allow="autoplay; fullscreen"></iframe>
40
+ ```
41
+
42
+ 3. 监听完成事件(见 §4),收到后关闭启动画面、进入 DSH 主界面。
43
+ 4. 如需改用户名/标题/文案,按 §3 改 `config.js`(或在加载前注入 `window.CONFIG`,见 §3.2)。
44
+
45
+ > 注意:`index.html` 引用的 `app.js`、`assets/*` 都是相对路径,**不能只拷 index.html**。
46
+
47
+ ---
48
+
49
+ ## 3. 参数
50
+
51
+ ### 3.1 `config.js` 字段(全部可选,保持结构即可)
52
+
53
+ ```js
54
+ window.CONFIG = {
55
+ username: '人类与AI的日常', // 「连接中:xxx」;必改
56
+ title: '什么是「模板」?', // 提示条标题
57
+ description: '模板是别人做好的启动动画…', // 提示条正文
58
+ duration: 25.92, // 源时间长度(成片会压到 20.33s)
59
+ startAt: 3.4, // 跳过片头黑场前的起点,非必要不改
60
+ loadingStart: 9.2, // 连接态→加载态切换点(源时间)
61
+ speedStart: 9.2, speed: 1.5, speedEnd: 25.45, // 加载段 1.5 倍速压缩
62
+ voiceY: 908, voiceWidth: 520, // DSH 行位置与宽度
63
+ egg: { at: 9.2 }, // 彩蛋判定点(源时间=加载段开始)
64
+ voice: {
65
+ orig1: 'assets/voice-orig1.wav', line1: '…', line2: '…',
66
+ line3: '…', orig3: '…', line4: '…',
67
+ sched: [[4.6,'orig1',2.65],[8.0,'line1',3.375],[12.8,'line2',3.232],[17.9,'line3',3.300],[25.45,'orig3',2.77]],
68
+ eggLineAt: 1.0, // 掉线画面出现后 1s 播 line4
69
+ line4Dur: 1.911,
70
+ eggReplayWait: 2.0 // line4 说完后等 2s 自动重播
71
+ },
72
+ progress: [[9.2,0],[10,0],[10.9,0],…,[25.45,100],[25.92,100]]
73
+ };
74
+ ```
75
+
76
+ - `username` 是必改项;其余文案/时长不改也能用。
77
+ - 改时长/语音时,`voice.sched` 与 `progress` 里的时间点要一起调(都是**源时间**,不是成片时间)。
78
+
79
+ ### 3.2 动态注入(宿主传参)
80
+
81
+ 模板本身**不监听** `postMessage` 配置(当前实现),有二选一方案:
82
+
83
+ 1. **服务端替换**:插件把 `config.js` 作为模板生成,写入宿主拿到的 username 等;
84
+ 2. **前置脚本**:iframe 指向插件自己的 `boot.html`,内容为:
85
+
86
+ ```html
87
+ <script>window.CONFIG = { /* 宿主参数 */ };</script>
88
+ <script src="division-bigfish-codex/config.js"></script>
89
+ <iframe src="division-bigfish-codex/index.html" …></iframe>
90
+ ```
91
+
92
+ > 注意:`config.js` 会覆盖此前设置的 `window.CONFIG`,若用方案 2 要把参数合并后再加载 `index.html`,或直接改 `config.js` 文本(最稳)。如希望模板原生支持 `postMessage` 配置,可在插件上下文里加约 10 行监听,我可以补。
93
+
94
+ ---
95
+
96
+ ## 4. 事件与运行控制
97
+
98
+ 宿主监听以下任意一条即可(三条通道同时触发):
99
+
100
+ ### 4.1 跳过
101
+
102
+ | 通道 | 形式 |
103
+ |---|---|
104
+ | DOM | `document.addEventListener('dsh-startup-skip', fn)` |
105
+ | postMessage | `{ type: 'dsh-startup-skip' }`(来自 iframe) |
106
+ | 全局函数 | `window.__DSH_SKIP__()`(同 window 内可用) |
107
+
108
+ ### 4.2 完成(进入 DSH 主界面)
109
+
110
+ | 通道 | 形式 |
111
+ |---|---|
112
+ | DOM | `document.addEventListener('dsh-startup-done', fn)` |
113
+ | postMessage | `{ type: 'dsh-startup-done' }` |
114
+ | 全局函数 | `window.__DSH_DONE__()` |
115
+
116
+ - 正常版:100% 后约 3.7s(含第五句播完 + 白闪)触发。
117
+ - 彩蛋版:点「重新连接」→ 重播 0–100% → 白闪后触发。
118
+ - 宿主收到后关闭启动层即可;**不要再等固定时长**(彩蛋会拉长总时长)。
119
+
120
+ ### 4.3 运行控制(可选)
121
+
122
+ ```js
123
+ window.__DSH_APP__ = { play, pause, seek, showEgg, eggFrame, flashDone };
124
+ ```
125
+
126
+ - `pause()`/`play()`:宿主切后台/失焦时暂停动画与音频。
127
+ - `seek(秒)`:仅调试用。
128
+ - `showEgg()` / `eggFrame(秒)` / `flashDone()`:调试试看彩蛋和结尾,不建议生产使用。
129
+
130
+ ---
131
+
132
+ ## 5. 彩蛋规则(务必保留)
133
+
134
+ - 概率:`app.js` 内 `EGG_CHANCE = 0.28`(写死;要可配置需改一行)。
135
+ - 判定:**每次启动只判定一次**,判定点 = 加载段开始(`egg.at`,源时间 9.2s)。
136
+ - 触发:**硬切**到掉线画面 → 静 1s → line4(1.911s)→ 静 2s → 自动重播进度条;用户点「重新连接」则**立即**重播。
137
+ - 掉线画面:黑纱 + 《战术大肥鱼®》错误框 + `DELTA / C-0-1302` + 「重新连接」+ 底部 DSH。
138
+ - 调试参数:`?egg=1` 强制触发;`?mute=1` 静音;`?play` 自动播放(render.html 用,index 默认自动)。
139
+ - **演示视频里的掉线点固定在 5.8s 只是为便于逐帧核对**,真实页面是随机的(0.28)。
140
+
141
+ ---
142
+
143
+ ## 6. 时间轴(成片时间,30fps)
144
+
145
+ 正常版(20.3337s = 610 帧):
146
+
147
+ | 段 | 时间 | 内容 |
148
+ |---|---|---|
149
+ | 连接态 | 0.000–5.800 | 圆环 + 「连接中:username」+ orig1(1.2s 起) |
150
+ | 加载态(1.5×) | 5.800–16.633 | 提示条 + 百分比 + line1(4.6)/line2(8.2)/line3(11.6) |
151
+ | 100% 静止 | 16.633–19.933 | orig3(16.633 起,2.77s),DSH 随语音显示 |
152
+ | 白闪 | 19.933–20.283 | 闪白 0.35s → done |
153
+
154
+ 彩蛋演示(25.2333s):连接 0–5.8 → 掉线画面 5.8–10.733(1s 静默 + line4 1.911 + 2s 静默)→ 重播 0–100% 10.733–21.566 → 100% 静止 → 白闪 → done。
155
+
156
+ ---
157
+
158
+ ## 7. 验收清单(接入后自测)
159
+
160
+ - [ ] iframe 加载后首帧纯黑,无旧画面闪帧。
161
+ - [ ] 1920×1080 下文字/圆环无裁切;缩放容器时保持 16:9 居中。
162
+ - [ ] 第一句「Alert. New agent login detected.」、第五句「welcome back Agent…」在音轨里可听(orig1/orig3)。
163
+ - [ ] DSH 波形与语音同进同出(100% 前不提前出,说完不残留)。
164
+ - [ ] 右上「跳过 ▸」可点,触发 skip 事件并关闭。
165
+ - [ ] 正常结束触发 done(约 20.3s;不要写死 25.9s)。
166
+ - [ ] 彩蛋(可 `?egg=1` 强制):硬切、1s 静 → line4 → 2s 静 → 自动重播;点「重新连接」立即重播;重播后 done。
167
+ - [ ] 彩蛋画面里没有残留波形/跳过按钮。
168
+ - [ ] 音频可播(iframe `allow="autoplay"` 或首次点击)。
169
+
170
+ ---
171
+
172
+ ## 8. 已知边界(不是 bug)
173
+
174
+ - 无 BGM;音频需用户交互后才可能出声(浏览器策略)。
175
+ - 演示视频的掉线点固定,真实页面随机 0.28。
176
+ - 模板未实现宿主→iframe 的 postMessage 配置通道(见 §3.2)。
177
+ - 历史版本:根目录还留有 v8–v12 成片与 `frames*` 缓存;**接插件只需 `templates/division-bigfish-codex/` 这一份**。
@@ -327,7 +327,7 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
327
327
  /* 第一幕/第二幕两个模板下拉:都含内置 archive-warm,且都有一项「内置/不播放」说明 */
328
328
  const templateSelects = () => walk(tree).filter((n) =>
329
329
  n.type === 'select' && Array.isArray(n.props.children) &&
330
- n.props.children.some((o) => o && o.type === 'option' && o.props.value === 'archive-warm') &&
330
+ n.props.children.some((o) => o && o.type === 'option' && (o.props.value === 'archive-warm' || o.props.value === 'whale-girl-scan')) &&
331
331
  n.props.children.some((o) => o && o.type === 'option' && /内置|不播放/.test(String(o.props.children))))
332
332
  /* archive-warm 被隐藏后上面的定位会失效:改用「有内置/不播放说明」来找两个下拉 */
333
333
  const templateSelectsAny = () => walk(tree).filter((n) =>
@@ -351,11 +351,11 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
351
351
  check('★ 两行各有 1 个「删除该动画」(共 2 个,始终显示)',
352
352
  rows.length === 2 && deleteAnimBtns().length === 2,
353
353
  '行 ' + rows.length + ' / 按钮 ' + deleteAnimBtns().length)
354
- check('★ 按钮固定在行内最右侧:两行 nowrap + space-between + gap 24px,按钮 marginLeft auto / minWidth 112px',
354
+ check('★ 行布局:nowrap + space-between + gap 24px;删除按钮 marginLeft 8px / minWidth 112px',
355
355
  rows.every((r) => r.props.style && r.props.style.flexWrap === 'nowrap' &&
356
356
  r.props.style.justifyContent === 'space-between' && r.props.style.gap === '24px') &&
357
357
  rows.every((r) => rowBtn(r).props.style &&
358
- rowBtn(r).props.style.marginLeft === 'auto' && rowBtn(r).props.style.minWidth === '112px'),
358
+ rowBtn(r).props.style.marginLeft === '8px' && rowBtn(r).props.style.minWidth === '112px'),
359
359
  JSON.stringify(rows.map((r) => rowBtn(r).props.style)))
360
360
  check('底部说明改成「本地删文件 / 内置只是隐藏可恢复」',
361
361
  texts3().some((t) => String(t).includes('本地动画会删除文件')) &&
@@ -480,7 +480,7 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
480
480
  check('本地删除不会产生「恢复已隐藏」按钮(它不可恢复)', !restoreBtn() && HIDDEN.length === 0)
481
481
  check('回填后第一幕按钮回到置灰', rowValue(rows[0]) === '' && rowBtn(rows[0]).props.disabled === true)
482
482
  check('收尾仍有 2 个「删除该动画」按钮', deleteAnimBtns().length === 2 && rows.length === 2)
483
- check('内置 archive-warm 仍可选择', templateSelects().length === 2 && hasOption(templateSelects()[1], 'archive-warm'))
483
+ check('内置 archive-warm 仍可选择', templateSelects().length === 2 && hasOption(templateSelects()[1], 'whale-girl-scan'))
484
484
  check('删除/隐藏流程共弹了 2 次 confirm', confirmCalls.length === confirmBefore + 2, String(confirmCalls.length))
485
485
  }
486
486
 
@@ -132,7 +132,7 @@ check('没有 Config 导出(普通对象不是 schemastery schema)', mod.Con
132
132
 
133
133
  console.log('\n[2] apply(ctx):路由与注入')
134
134
  mod.apply(ctx)
135
- check('注册了 40 条路由(开机动画 29 + 聊天壁纸 11)', ROUTES.size === 40, [...ROUTES.keys()].join(' | '))
135
+ check('注册了 45 条路由(开机动画 34 + 聊天壁纸 11)', ROUTES.size === 45, [...ROUTES.keys()].join(' | '))
136
136
  check('注册了 1 个 tapIndex', TAPS.length === 1)
137
137
  check('用了 ctx.effect 管理生命周期(开机动画 1 + 壁纸 11)', effects === 12, String(effects))
138
138
 
@@ -452,6 +452,28 @@ console.log('\n[6i] v2.13.19:6 套音色包清单与只读音频路由')
452
452
  check('未注册音色 z -> 404', vz.statusCode === 404, String(vz.statusCode))
453
453
  }
454
454
 
455
+ console.log('\n[6j] v2.14.0:用户自备 BGM 的路由与默认值')
456
+ {
457
+ const c = await call('exact /dsh-startup/config')
458
+ const cj = JSON.parse(c.body)
459
+ check('BGM 默认关闭', cj.config.bgmEnabled === false, String(cj.config.bgmEnabled))
460
+ check('BGM 默认无文件 / 100 BPM / 音量 0.5', cj.config.bgmFile === '' && cj.config.bgmBpm === 100 && cj.config.bgmVolume === 0.5,
461
+ JSON.stringify({ f: cj.config.bgmFile, b: cj.config.bgmBpm, v: cj.config.bgmVolume }))
462
+ const lst0 = JSON.parse((await call('exact /dsh-startup/music/list')).body)
463
+ check('GET /music/list ok 且初始为空', lst0.ok === true && Array.isArray(lst0.files) && lst0.files.length === 0, JSON.stringify(lst0.files))
464
+ const small = Buffer.from('RIFF' + '\0'.repeat(200))
465
+ const up = JSON.parse((await call('exact /dsh-startup/music/upload', 'POST', small, '/dsh-startup/music/upload?name=test-music.wav')).body)
466
+ check('POST /music/upload ok', up.ok === true && /^music-/.test(up.file || ''), JSON.stringify(up))
467
+ const lst1 = JSON.parse((await call('exact /dsh-startup/music/list')).body)
468
+ check('上传后 list 出现 1 首', lst1.files.length === 1 && lst1.files[0].file === up.file, JSON.stringify(lst1.files))
469
+ const f = await call('exact /dsh-startup/music/file', 'GET', undefined, '/dsh-startup/music/file?name=' + encodeURIComponent(up.file))
470
+ check('GET /music/file 200 + audio/wav', f.statusCode === 200 && /audio\/wav/.test(f.headers['Content-Type']), f.statusCode + ' ' + f.headers['Content-Type'])
471
+ const bad = await call('exact /dsh-startup/music/file', 'GET', undefined, '/dsh-startup/music/file?name=../../package.json')
472
+ check('非法文件名 -> 404', bad.statusCode === 404, String(bad.statusCode))
473
+ const del = JSON.parse((await call('exact /dsh-startup/music/delete', 'POST', JSON.stringify({ file: up.file }))).body)
474
+ check('POST /music/delete ok', del.ok === true, JSON.stringify(del))
475
+ }
476
+
455
477
  console.log('\n[7] XSS 边界:身份名称里的尖括号不会逃出内联脚本')
456
478
  await call('exact /dsh-startup/config', 'POST', JSON.stringify({ patch: { identity: '</script><img src=x onerror=alert(1)>' } }))
457
479
  const payload = inlinePayload(renderRows(before, collectRows()))
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
 
4
- const root = process.cwd();
5
+ const root = fileURLToPath(new URL('..', import.meta.url));
5
6
  const buf = fs.readFileSync(path.join(root, 'package.json'));
6
7
  if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
7
8
  throw new Error('package.json 带 UTF-8 BOM,DSH 会拒绝加载');
@@ -27,4 +28,91 @@ for (const needle of ['TargetPkg', 'CheckOnly', 'DSH_BOOT_SUITE_ROLLBACK', '回
27
28
  for (const f of ['line-1.mp3', 'line-6.mp3', 'dry-line-1.mp3']) {
28
29
  if (fs.existsSync(path.join(root, 'lib/assets', f))) throw new Error('旧 TTS 配音素材未删除:' + f);
29
30
  }
31
+
32
+ /* v2.15.5:全景大肥鱼-含彩蛋版(重命名 + 结尾波形 + 模板内配音效果) */
33
+ const tplJson = JSON.parse(fs.readFileSync(path.join(root, 'templates/division-bigfish-codex/template.json'), 'utf8'))
34
+ if (tplJson.name !== '全景大肥鱼-含彩蛋版') throw new Error('模板名称未更新:' + tplJson.name)
35
+ const bigfishApp = fs.readFileSync(path.join(root, 'templates/division-bigfish-codex/app.js'), 'utf8')
36
+ for (const needle of ['attachVoiceFx', 'window.__WAIT_START__', 'VOICE_FX_PRESETS']) {
37
+ if (!bigfishApp.includes(needle)) throw new Error('模板内配音效果/结尾波形缺少:' + needle)
38
+ }
39
+ const hostStartup = fs.readFileSync(path.join(root, 'lib/host-startup.js'), 'utf8')
40
+ if (!hostStartup.includes("bigfishVoiceFx: 'none'")) throw new Error('host-startup 缺少 bigfishVoiceFx 默认值')
41
+ if (!hostStartup.includes("window.CONFIG.voiceFx='")) throw new Error('host-startup 未注入模板 voiceFx')
42
+ const clientJs = fs.readFileSync(path.join(root, 'lib/client.js'), 'utf8')
43
+ for (const needle of ['bigfishVoiceFx', '全景大肥鱼-含彩蛋版']) {
44
+ if (!clientJs.includes(needle)) throw new Error('设置页缺少:' + needle)
45
+ }
46
+ /* v2.15.6:掉线彩蛋必须等用户点【重新连接】,不许自动 reconnect */
47
+ if (bigfishApp.includes('setTimeout(reconnect')) throw new Error('彩蛋仍有自动 reconnect 定时器')
48
+ if (!bigfishApp.includes('必须等用户点【重新连接】')) throw new Error('彩蛋等待逻辑缺少注释锚点')
49
+ const bigfishCfg = fs.readFileSync(path.join(root, 'templates/division-bigfish-codex/config.js'), 'utf8')
50
+ if (bigfishCfg.includes('eggReplayWait')) throw new Error('config.js 仍残留 eggReplayWait 自动重播字段')
51
+ /* v2.15.7:结尾等待配音时必须继续渲染波形 */
52
+ if (!bigfishApp.includes('if(!playing&&t>=cfg.duration-0.05)render();')) throw new Error('结尾等待期缺少渲染调用,波形会再次冻住')
53
+ /* v2.15.8:模板配音音量 + iframe 模板 BGM */
54
+ if (!hostStartup.includes('bigfishVoiceVolume: 50')) throw new Error('host-startup 缺少配音音量默认值')
55
+ if (!hostStartup.includes('window.CONFIG.voiceVolume=')) throw new Error('host-startup 未注入模板配音音量')
56
+ if (!bigfishApp.includes('a.volume=voiceVol')) throw new Error('模板配音未应用音量')
57
+ if (!bigfishApp.includes('dsh-startup-voice')) throw new Error('模板未上报配音闪避事件')
58
+ const splashJs = fs.readFileSync(path.join(root, 'lib/splash.js'), 'utf8')
59
+ if (!splashJs.includes('dsh-startup-voice')) throw new Error('splash 未处理模板配音闪避')
60
+ if (!/engine === 'iframe'[\s\S]{0,160}bgm\.start\(\)/.test(splashJs)) throw new Error('iframe 模板未启动 BGM')
61
+ if (!clientJs.includes('配音音量')) throw new Error('设置页缺少配音音量')
62
+ if (!clientJs.includes('bigfishVoiceVolume')) throw new Error('设置页缺少 bigfishVoiceVolume')
63
+ if (!clientJs.includes('style: (isArchiveWarm || isBigfish)')) throw new Error('BGM 组未对全景大肥鱼显示')
64
+ /* v2.15.9:无第二幕不切第二段音乐、BGM 循环开关、跳过按钮位置 */
65
+ if (!hostStartup.includes('bgmLoop: true')) throw new Error('host-startup 缺少 bgmLoop 默认值')
66
+ if (!hostStartup.includes("'bgmLoop'")) throw new Error('host-startup BOOL_KEYS 缺 bgmLoop')
67
+ if (!splashJs.includes('C.bgmLoop !== false')) throw new Error('splash 未使用 bgmLoop')
68
+ if (!/if \(!TEMPLATE2[\s\S]{0,420}bgm\.toSecond\(\)/.test(splashJs)) throw new Error('splash 在无第二幕时仍提前切第二段音乐')
69
+ if (!clientJs.includes('hasSecondAct')) throw new Error('设置页缺少第二幕判断')
70
+ if (!clientJs.includes('bgmLoop')) throw new Error('设置页缺少循环开关')
71
+ const skipCss = fs.readFileSync(path.join(root, 'templates/division-bigfish-codex/style.css'), 'utf8')
72
+ if (!skipCss.includes('top:64px;right:72px')) throw new Error('跳过按钮位置未调整')
73
+ /* v2.15.10:掉线时机随机、彩蛋截断 BGM、重连不叠音 */
74
+ if (!bigfishApp.includes('dsh-startup-bgm')) throw new Error('模板未上报 BGM 彩蛋信号')
75
+ if (!bigfishApp.includes('resetPlayedAfter')) throw new Error('重连未按时间点重置语音')
76
+ if (!bigfishApp.includes('eggTimeForPct(Math.random()*100)')) throw new Error('掉线时机未按进度百分比随机')
77
+ if (!splashJs.includes('bgm cut')) throw new Error('splash 缺少 BGM 截断')
78
+ if (!splashJs.includes('dsh-startup-bgm')) throw new Error('splash 未处理 BGM 彩蛋信号')
79
+ if (!bigfishCfg.includes('egg:{}')) throw new Error('config.js 仍固定 egg.at')
80
+ /* v2.15.11:模板不再自己做白闪,结尾转场由插件接管 */
81
+ for (const f of ['index.html', 'render.html']) {
82
+ const html = fs.readFileSync(path.join(root, 'templates/division-bigfish-codex', f), 'utf8')
83
+ if (html.includes('id="flash"')) throw new Error(f + ' 仍有模板白闪节点 #flash')
84
+ }
85
+ if (bigfishApp.includes('flashEl')) throw new Error('app.js 仍引用模板白闪 flashEl')
86
+ if (bigfishApp.includes('flashEl')) throw new Error('app.js 仍残留白闪动画')
87
+ if (skipCss.includes('#flash')) throw new Error('style.css 仍残留 #flash 样式')
88
+ if (!/return done\.then\(function \(\) \{[\s\S]{0,260}transOut\(transType\('Out'\), finish\)/.test(splashJs)) throw new Error('iframe 模板收尾未交给插件退场转场')
89
+ /* v2.15.12:结尾等配音时圆环继续转 */
90
+ if (!bigfishApp.includes('const rt=t+waitSec')) throw new Error('结尾圆环未使用连续旋转时钟 rt')
91
+ if (!bigfishApp.includes('rotate(${rt*5})')) throw new Error('外圈未使用 rt 旋转')
92
+ if (!bigfishApp.includes('rotate(${-rt*12})')) throw new Error('内圈未使用 rt 旋转')
93
+ if (!bigfishApp.includes('Math.sin(rt*6.54)')) throw new Error('外圈脉冲未使用 rt')
94
+ if (!/function flashDone\(\)\{hideEgg\(\);doneEmit\(\);\}/.test(bigfishApp)) throw new Error('flashDone 不应再清零旋转时钟')
95
+ /* v2.15.13:配音中文字幕 */
96
+ if (!bigfishApp.includes('cfg.voice.subs')) throw new Error('模板未使用字幕映射 cfg.voice.subs')
97
+ if (!bigfishApp.includes('subtitle')) throw new Error('模板未更新 #subtitle')
98
+ const bigfishHtml = fs.readFileSync(path.join(root, 'templates/division-bigfish-codex', 'index.html'), 'utf8')
99
+ if (!bigfishHtml.includes('id="subtitle"')) throw new Error('index.html 缺少 #subtitle 字幕节点')
100
+ if (!bigfishCfg.includes('警告:检测到新特工登录')) throw new Error('config.js 缺少中文字幕映射')
101
+ if (!bigfishCfg.includes("line2:'装备与技能配置已验证。倒计时十秒。'")) throw new Error('config.js line2 字幕未包含完整句')
102
+ if (!bigfishCfg.includes("line3:'网络同步完成。正在移交权限。'")) throw new Error('config.js line3 字幕不正确')
103
+ if (!bigfishCfg.includes("reconnect:'检测到特工重新连接。'")) throw new Error('config.js 缺少重连接字幕')
104
+ if (!bigfishCfg.includes("line4:'警告:特工已离线。'")) throw new Error('config.js 缺少彩蛋 line4 字幕')
105
+ if (!bigfishApp.includes('renderVoiceRow')) throw new Error('模板未使用整行字幕布局 renderVoiceRow')
106
+ if (!fs.existsSync(path.join(root, 'templates/division-bigfish-codex/assets/whale-badge-line.png'))) throw new Error('缺少鲸鱼徽章图标 whale-badge-line.png')
107
+ /* v2.15.14:重连提示音独占一整行,播完再继续,且不再续播断句 */
108
+ if (!bigfishApp.includes('重连接提示音独占一整行')) throw new Error('app.js 缺少重连提示音独占一行的实现')
109
+ if (!bigfishApp.includes('const cueOn=')) throw new Error('渲染未使用重连提示音分支 cueOn')
110
+ if (!bigfishApp.includes('playing&&!cueGate')) throw new Error('重连期间未门控后续配音(cueGate)')
111
+ if (!bigfishApp.includes('played.line2=false;played.line3=false;played.orig3=false')) throw new Error('重连后未重置后续配音的播放标记')
112
+ if (/resetPlayedAfter\(cfg\.loadingStart\)/.test(bigfishApp)) throw new Error('重连仍会续播断句(resetPlayedAfter)')
113
+ if (!bigfishApp.includes('cueFailTimer')) throw new Error('重连提示音缺少播放失败的兜底解除门控')
114
+ /* v2.15.15:重连提示音也要走插件的配音效果;line3 顺延,避免和 line2 叠音 */
115
+ if (!bigfishApp.includes("attachVoiceFx('reconnect')")) throw new Error('重连提示音未接入配音效果')
116
+ if (!bigfishApp.includes("[20.0,'line3',3.300]")) throw new Error('app.js 默认时间线未把 line3 顺延到 20.0s')
117
+ if (!bigfishCfg.includes("[20.0,'line3',3.300]")) throw new Error('config.js 时间线未把 line3 顺延到 20.0s')
30
118
  console.log('ok package.json version=' + pkg.version + ',无 BOM,关键文件齐全(含更新前回滚 checkpoint)');
@@ -336,6 +336,8 @@ console.log('\n[8] 终幕用真声欢迎语(来自 Video Project 3 的 line-6
336
336
  check('配音音色可切换:voicePack 为 f–k 时走 voice:<pack>:line-N 路由',
337
337
  /C\.voicePack/.test(src) && /'voice:' \+ pack \+ ':' \+ asset/.test(src)
338
338
  && /tplAssets && tplAssets\[asset\]/.test(src))
339
+ check('全局 line-N 无人声包时也改走 voice 路由(不再请求已删除的 lib/assets)',
340
+ src.includes("'/dsh-startup/voice/' + pack + '/' + name + '.mp3'") && src.includes("if (pack === 'builtin') pack = 'f'"))
339
341
  check('5 套声音效果预设都在,播放链路按 C.voiceFx 叠加',
340
342
  ['ai-mech', 'robot', 'radio', 'oldtv', 'dark'].every((k) => src.includes("'" + k + "':"))
341
343
  && /playAssetAndWait\(useAsset, 0\.85, isGlobalLine \? fx : 'none'\)/.test(src))
@@ -463,7 +463,7 @@ ok(intervals.every((t) => t.cleared) || intervals.length === 0, '旧的 interval
463
463
  intervals.map((t) => (t.cleared ? 'cleared' : 'live')).join(','))
464
464
 
465
465
  console.log('\n=== ⑦ v2.13.0 测试钩子 / BUILD ===')
466
- ok(mod.__BUILD === 'cw-44', '__BUILD = cw-44(排查浏览器跑的是不是新包)', String(mod.__BUILD))
466
+ ok(mod.__BUILD === 'cw-63', '__BUILD = cw-63(排查浏览器跑的是不是新包)', String(mod.__BUILD))
467
467
  ok(mod.__VIDEO_ID === 'dsh-cw-video', '__VIDEO_ID = dsh-cw-video', String(mod.__VIDEO_ID))
468
468
  ok(typeof mod.__startShuffle === 'function', '__startShuffle 导出')
469
469
  ok(typeof mod.__stopShuffle === 'function', '__stopShuffle 导出')
@@ -50,7 +50,7 @@ const ctx = {
50
50
  on() { return () => {} },
51
51
  get(name) {
52
52
  if (name === 'webServer') return webServer
53
- if (name === 'connection') return { requestRejection: () => undefined }
53
+ if (name === 'connection') return { requestRejection: (req) => (String((req && req.url) || '').includes('__deny=1') ? 401 : undefined) }
54
54
  return undefined
55
55
  },
56
56
  effect(fn) { effects++; const d = fn(); return () => { if (typeof d === 'function') d() } },
@@ -581,6 +581,12 @@ try {
581
581
  check('remove 后 /videos.roots 不再含该目录',
582
582
  !(rootsRes.json.roots || []).some((root) => root.path.toLowerCase().includes('custom-loops')), JSON.stringify(rootsRes.json.roots))
583
583
 
584
+ /* v2.14.0:壁纸路由接入信任栅栏(connection.requestRejection 返回状态码时直接拒绝) */
585
+ const deniedRes = await callJson('exact /chat-wallpaper/config', 'GET', undefined, '/chat-wallpaper/config?__deny=1')
586
+ check('★ 壁纸路由受信任栅栏保护:requestRejection 返回 401 时拒绝', deniedRes.res.statusCode === 401 && deniedRes.res.body === '', String(deniedRes.res.statusCode))
587
+ const allowedRes = await callJson('exact /chat-wallpaper/config', 'GET', undefined, '/chat-wallpaper/config')
588
+ check('栅栏放行普通请求', allowedRes.res.statusCode === 200, String(allowedRes.res.statusCode))
589
+
584
590
  } catch (e) {
585
591
  fail++
586
592
  console.log(' FAIL 测试本身抛异常:' + String((e && e.stack) || e))
package/tools/README.md CHANGED
@@ -23,5 +23,5 @@
23
23
 
24
24
  ## 注意
25
25
 
26
- - 三个脚本都会**先要求桌面端完全退出**,否则直接退出不干活 —— 这是刻意的,避免在进程锁着 profile 时写半截。
26
+ - 三个脚本默认会**先检查桌面端是否完全退出**,运行中会拒绝(避免在进程锁着 profile 时写半截);确认要热装可加 `-Force`,装完重启 DSH。
27
27
  - 卸装 / 回滚都不会动 `~/.dsh/dsh-startup.json` 和 `~/.dsh/chat-wallpaper.json`(回滚会从备份里恢复它们)。