Communitygithub.com

Hermes 启动电影技能

在 Hermes 桌面版启动时播放电影,并可叠加自定义音轨。

Hermes 启动电影技能 是什么?

此技能将 Hermes 桌面版现有的启动电影从仅在首次和来宾引导时播放,改为**每次启动**都播放。它允许开发者叠加自定义画外音,而不会干扰内置音效。该技能需要修改三个关键文件:'intro-reveal' 存储、覆盖组件和 Electron 主进程,以确保电影始终渲染。它提供了详细的打包说明,用于将更改部署到已签名的应用程序,并包含用于测试的浏览器沙箱。技能还说明了如何使用 `<audio>` 元素(而不是 WebAudio)添加画外音,以避免 `file://` 协议下的 CORS 问题,并阐述了防止无限循环的 'playedThisSession' 标志等关键陷阱。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add xbmaxx/hermes-intro-cinematic

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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。

每次启动重播——三项修改,缺一不可

  1. src/store/intro-reveal.ts — isIntroRevealEnabled() 必须不再要求来宾引导;shouldPlayFirstRunIntro() 在非引导环境下返回 !playedThisSession。必须使用模块级 playedThisSession 标志:每当阶段返回到 'hidden' 时,门控的 effect 会重新运行,而电影播放结束正是如此——没有标志就会陷入无限循环。
  2. src/components/intro-reveal/intro-root.tsx — 覆盖窗口检查 isOnboardingEnabled() 并会在画面前返回。将其改为与主渲染器相同的谓词。
  3. 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 中;更新后需要重新运行安装程序。

相关技能