Hermes 启动电影
Hermes 桌面版已经附带了一部 22 秒的启动电影——包含七个节拍、一个动态立方体视口、并行代理和 Nous 品牌,并配有合成音效。它存在,但几乎没人看第二遍:播放资格由 guestOnboardingEnabled && !seen 控制。
本技能让电影在每次启动时播放,并允许你在内置提示音之上叠加自己的画外音轨。
使用时机
- “让 Hermes 启动动画每次都播放。”
- “把我的音频放到启动动画上——要求叠加,而不是替换音效。”
- 任何对
apps/desktop/src/components/intro-reveal/的修改。
电影现有结构
| 组件 | 路径 |
|---|---|
| 门控(何时播放) | apps/desktop/src/components/intro-reveal/index.tsx |
| 资格与阶段存储 | apps/desktop/src/store/intro-reveal.ts |
| 覆盖层启动 | src/components/intro-reveal/intro-root.tsx |
| 时钟、评分、画布 | use-intro-clock.ts, timeline.ts, viewport-cube.ts |
| 合成提示 | sound.ts |
| 原生窗口 | electron/intro-reveal-window.ts, electron/main.ts |
时间计算:INTRO_TOTAL_MS = 17 600 是评分时间;实际时间为 × INTRO_PACE (1.25) = 22 000 ms,加上 INTRO_EXIT_MS 900 ms → 总计 23 125 ms。
每次启动重播——三项修改,缺一不可
src/store/intro-reveal.ts—isIntroRevealEnabled()必须不再要求来宾引导;shouldPlayFirstRunIntro()在非引导环境下返回!playedThisSession。必须使用模块级playedThisSession标志:每当阶段返回到'hidden'时,门控的 effect 会重新运行,而电影播放结束正是如此——没有标志就会陷入无限循环。src/components/intro-reveal/intro-root.tsx— 覆盖窗口检查isOnboardingEnabled()并会在画面前返回。将其改为与主渲染器相同的谓词。electron/main.ts—introRevealController构造时使用enabled: GUEST_ONBOARDING,导致主进程拒绝open。将其设为true;存活状态由渲染器决定。
引导流程的 beginOnboardingFlow() / queueGuideAfterIntro() / takeGuideShape() 都自带有 isOnboardingEnabled() 守卫,因此当引导关闭时它们不会执行——无需额外修改。
关闭开关:localStorage['hermes-intro-reveal-always-v1'] = '0',或使用 HERMES_SKIP_INTRO=1 / --skip-intro 启动。
叠加音频轨
将文件放在 apps/desktop/public/intro-voiceover.mp3(vite 会将 public/ 原样复制到 dist/),然后在 sound.ts 中:
export function startVoiceover(cutAtMs: number): { stop: () => void }
——创建一个 Audio 元素,设置 volume,启动它,并使用 setTimeout(stop, cutAtMs - 300) 以便最后 300ms 渐弱而非突然中断。在 use-intro-clock.ts 中紧邻 startPad() 调用它,并在 effect 清理时停止。
使用 <audio> 元素,切勿使用 fetch() + decodeAudioData()。 打包后的渲染器运行在 file:// 协议下,fetch 和 decodeAudioData 会被 CORS 阻止;媒体元素可以正常加载 file:// 资源。
两条音轨天然独立——元素音频和 WebAudio 提示在操作系统层面混合,无需任何回避逻辑。将 $hapticsMuted 连接到两者,并订阅而非只读取一次,否则静音切换会不同步。
截止点:
const cutMs = Math.round((INTRO_TOTAL_MS + INTRO_EXIT_MS) * INTRO_PACE) // 23 125
部署到打包应用
只修改源码不重新打包 asar 不会生效。这里有两个陷阱值得检查:
找到用户实际启动的应用程序——不要假定在 /Applications。
ps aux | grep '[H]ermes.app/Contents/MacOS/Hermes'
defaults read com.apple.dock persistent-apps | grep -o '/[^"]*Hermes\.app'
Hermes 更新包通常位于 ~/.hermes/hermes-agent*/apps/desktop/release/mac-arm64/Hermes.app,Dock 经常指向这个路径,而 /Applications/Hermes.app 可能是一个过时的 0.0.0 副本。请用 CFBundleShortVersionString 确认(0.17.x 是正式版,0.0.0 不是)。
也要刷新 app.asar.unpacked/dist/。 打包构建将 dist/** 列入 asarUnpack 清单,resolveRendererIndex() 会优先使用那个副本——仅重新打包 asar 会被过时的解压树覆盖。
npm install --workspace apps/desktop --ignore-scripts \
--engine-strict=false --min-release-age=0
cd apps/desktop && node ../../node_modules/vite/bin/vite.js build
node scripts/bundle-electron-main.mjs # 主进程修改也需要这一步
asar extract "$RES/app.asar" "$WORK"
rm -rf "$WORK/dist" && cp -R apps/desktop/dist "$WORK/dist"
asar pack "$WORK" "$WORK/app.asar"
mv -f "$WORK/app.asar" "$RES/app.asar"
cp -R apps/desktop/dist/. "$RES/app.asar.unpacked/dist/"
codesign --force --deep -s - "$APP" # 临时签名;Hermes 默认使用临时签名
在慢速网络下 --ignore-scripts 很重要:不需要下载 Electron 二进制文件来构建或重新打包,只有运行开发环境才需要。
无需 Electron 即可验证
在普通浏览器中渲染电影——无需打包,无需下载二进制文件:
cd apps/desktop && node ../../node_modules/vite/bin/vite.js --host 127.0.0.1 --port 5174
page.add_init_script("""
window.hermesDesktop = {
guestOnboardingEnabled: true, skipIntro: false,
introReveal: { ready(){}, skip(){},
open(){ return Promise.resolve() }, close(){ return Promise.resolve() },
onSkip(){ return () => {} }, onClosed(){ return () => {} } }
};
""")
page.goto("http://127.0.0.1:5174/?win=intro")
page.evaluate("import('/src/components/intro-reveal/intro-root.tsx').then(m => m.mountIntroReveal())")
路由自身的挂载调用在没有桥接时不会执行;真正的渲染需要先模拟桥接再手动导入。要检查音频轨,模拟 window.Audio 并记录实例——断言 currentTime 随着电影推进,并且在截止点 paused === true, volume === 0。
值得保留在 src/store/intro-reveal.test.ts 中的契约测试:
- 即使在设置了
hermes-intro-reveal-seen-v1的情况下,正常启动也会播放 - 只播放一次——
finishIntroReveal()不得重新触发 - 支持 opt-out 键和
skipIntro启动标志 - 不影响引导的首播语义
陷阱
- 移除
playedThisSession会导致无限重播循环,而非无 Bug 的启动。 INTRO_TOTAL_MS是评分时间。忘记INTRO_PACE会使画外音提前 4.4 秒停止。- 主进程的
enabled标志和覆盖层自身的守卫是两个独立的门;错过任何一个都不会渲染,且没有错误提示。 - 四个独立层确保屏幕恢复(正常关闭、Esc + 1.2 秒回退、本地死机监视器、34 秒主进程看门狗)。修改时间时不要移除了其中一个。
hermes update会将应用替换为官方构建。补丁会保留在 git 中;更新后需要重新运行安装程序。