Journey demo
编写一个旅程,测试它,剪辑它。Claude 在 Chrome 中运行一个用户旅程,将每个屏幕保存为一个冻结的 HTML 页面,然后将这些帧布局成一个故事板,用户可以在浏览器中编辑,只有当他们批准后,才使用 HyperFrames 渲染出 MP4 视频。整个过程不是屏幕录制,因此任何屏幕上的文字都可以在以后更改,而无需重新运行旅程。
真相来源: STORYBOARD.md + snapshots/*.html 位于演示文件夹中。其他所有内容都是生成的。
需要
- Chrome 并安装 Claude in Chrome 扩展(用于运行旅程和捕获)
- Node 18+,以及
npx hyperframes(用于静态帧和渲染;无需安装步骤) - 本技能中的脚本:
scripts/freeze.js(捕获)、scripts/demo.mjs(构建和可编辑的故事板)。通过路径运行它们:node <本技能路径>/scripts/demo.mjs …
演示文件夹
除非用户指定其他位置,否则默认为用户仓库中的 docs/demos/<slug>/:
STORYBOARD.md 计划:帧、说明文字、点击、时间、注释 (编辑)
snapshots/*.html 捕获的屏幕 (仅编辑文字)
index.html scenes/ boards/ .build/ 由 demo.mjs 生成 (从不编辑)
out/<slug>.mp4 渲染结果
建议将 scenes/ .build/ boards/stills/ out/ 添加到 .gitignore 中。
1. 旅程
当存在旅程文件时使用它(docs/journeys/*.md:Start:、编号步骤、Expect: 检查、> Note: 行)。如果安装了 browser-journeys 技能,它将拥有该格式及其运行规则,你需要遵循它们。还没有旅程?先与用户一起编写一个:演示的质量取决于步骤的质量。
在运行之前,在 一条 消息中询问,并给出你的建议:
- 主机: 本地或预发布环境;绝不使用生产环境,除非用户明确要求。
- 分支: 当旅程提供选择时(从库中复制或编写新内容、保存的卡片或新卡片),演示展示哪条路径。默认:最短的快乐路径。
- 数值: 旅程的测试值(“旅程测试——请忽略”)或你建议的友好演示值。默认:建议友好演示值,并在所有出现的地方使用它们。
- 消息和受众: 两者都从旅程中草拟;用户确认。
- 副作用: 说明在该主机上运行会创建什么(账户、订单、出价)以及旅程的清理步骤(如果有)。
为另一个主机编写的旅程(Start: local、种子命令、从开发日志读取的代码)?其设置行属于该主机:询问该主机上的等效内容,切勿猜测。
2. 运行并捕获
- 将窗口大小调整为
resize_window1440×900,并保持该大小在整个运行过程中不变:每个帧必须共享一个视口,构建过程会拒绝混合尺寸。 - 按照旅程的要求执行每一步。检查每一个
Expect:;如果失败,停止并报告。绝不在损坏的流程上构建演示。捕获运行不需要以可观看的速度进行:只要等待页面稳定下来后再冻结即可。 - 登录和角色切换由用户负责。 说“以 <角色> 身份登录,完成后告诉我”,等待,然后继续。如果旅程打印到开发日志中的代码,你可以输入;绝不输入密码或来自他人收件箱的代码。不要冻结登录屏幕(它们显示电子邮件):故事板从登录前直接跳到登录后。
- 选择帧:每次点击一个帧。 帧是观众需要跟随流程而点击之前的屏幕。输入、勾选和选择在冻结之前完成,因此帧显示它们已完成;当帧应显示空字段时,在输入前冻结。通过 URL 访问或没有内容可点击的屏幕是一个没有
click的帧(它会淡入)。跳过设置和登录。目标是 4-8 帧,并且始终冻结最终屏幕。 - 冻结: 将
scripts/freeze.js的全部内容粘贴到页面中,使用javascript_tool(每次页面加载一次;typeof window.__freeze可以告诉你),然后执行await window.__freeze('03-open-the-orbit')(两位数字顺序,然后是一个简短标识)。将其返回的viewport与第一个帧的进行比较;如果不同(由于下载或信息栏打开),关闭该栏并重新冻结。 - 每次冻结会将
<name>.html下载到~/Downloads。运行结束后将它们移动到snapshots/。再次冻结相同名称的文件会使 Chrome 保存<name> (1).html:先删除旧的下载文件。如果 Chrome 要求允许多次下载,用户点击“允许”。
3. 编写 STORYBOARD.md
HyperFrames 的故事板格式。每个冻结的屏幕一个帧:
---
message: 在一分钟内在 Orbit 中打开
受众: 正在决定是否尝试 ShoutOrbit 的 YouTube 创作者
旅程: docs/journeys/create-an-orbit.md (步骤 4-5, 预发布环境, 2026-11-13)
url: staging.shoutorbit.com
状态: 草稿
---
## 帧 3: 打开 Orbit (8-12s)
- duration: 4s
- snapshot: 03-open-the-orbit.html
- click: 打开 Orbit
- scene: 日期和报价自动填充
表单底部,日期已填充。光标按下“打开 Orbit”。
开放问题:标题字段仍显示占位符。是在屏幕上打印出来,还是保留?
click: 元素的 完整标签,完全一致: 其可见文本、值、占位符或 aria-label (例如“Place bid · $25”,而不是“Place bid”)。构建过程会在每次运行中查找它,如果找不到则会大声失败。对于没有点击的帧,省略它。duration: 在屏幕上的秒数,3-5 秒;省略时默认为 4 秒。scene: 屏幕上的说明文字:简短,关于观众得到什么(“日期自动填充”),而不是他们按了什么。- 计划行:帧显示什么以及光标做什么,用简单语言描述。
- 开放问题: 每行一个用户必须决定的事项。检查每个冻结的屏幕:测试或垃圾数据;个人数据(电子邮件、真实姓名、账号);应放置真实值的位置显示占位符;与屏幕矛盾的说明文字(“直播”但显示“尚未接受出价”);过时的日期。提出这些问题;绝不悄悄修复它们。
4. 构建并交出故事板
node <技能路径>/scripts/demo.mjs serve docs/demos/<slug> --port 4321 # 在后台运行
它会构建,然后服务 http://127.0.0.1:4321/。为用户打开它(Chrome 或 open),然后停止。在故事板上,他们可以更改说明文字、点击操作、秒数、标题和顺序、删除帧、为 Claude 写注释、重新输入捕获页面的文字(编辑页面文字;重命名点击元素会同时保留点击操作),并按下 批准渲染。每次保存都会重新构建静态帧。
5. 处理注释
当用户说他们留下了注释时,阅读 STORYBOARD.md。对于每个 > Note: 行:
| 他们要求 | 更改 |
|---|---|
| 说明文字、时间、顺序、点击 | 帧在 STORYBOARD.md 中的键 |
| 页面上的不同文字 | 该 snapshots/*.html 中的文本(仅文本:保留标签和类) |
| 未捕获的屏幕或不同状态 | 再次运行该旅程步骤并冻结 |
| 流程无法展示的内容 | 说明情况;提供最接近的真实版本 |
然后将注释转换为 > Done: <更改了什么>(故事板将这些显示为已完成),在前置元数据中设置 status: draft(只有用户批准),运行 node <技能路径>/scripts/demo.mjs build docs/demos/<slug>,并告知用户刷新故事板。服务器可以保持运行。
6. 渲染
仅当前置元数据为 status: approved,或用户在聊天中说“开始”时:
cd docs/demos/<slug> && npx hyperframes check && npx hyperframes render -o out/<slug>.mp4
check 关于子合成、轨道密度和无声影片的警告是预期的。为用户打开 MP4 文件。如果之后 UI 发生变化,再次运行该旅程,重新冻结屏幕已更改的帧,保留故事板,然后渲染:这就是整个更新过程。
常见错误
| 症状 | 原因和修复 |
|---|---|
| 光标位于目标下方或旁边 | 帧是在不同窗口大小或页面滚动中途捕获的。重新在一种大小下捕获。 |
构建过程中出现 nothing labelled "…" | click 文本与屏幕不完全匹配(大小写、空格、→)。从页面复制文本。 |
probe viewport was … | Headless Chrome 的窗口 chrome 发生了变化;调整 demo.mjs 中的 CHROME_UI。 |
| 帧显示空字段 | 在输入前冻结。先填充,然后冻结。 |
| 帧中缺少图片或字体 | 跨域请求没有 CORS 头,因此 freeze.js 保留了 URL;它们仅在主机在线时加载。将其作为一个开放问题记录。 |
| Stripe 或其他 iframe 空白 | freeze.js 会丢弃 iframe。在它们之前或之后冻结屏幕,或者说明缺少什么。 |
Chrome not found | 设置 CHROME=/path/to/chrome。 |
隐私
一个快照包含屏幕上的所有内容,图片被内联。在任何人分享故事板、文件夹或视频之前,检查帧中是否有个人数据,并将其作为开放问题提出。