SuperAPI AI 工具集

mobile-demo-record

测试与质量

查看项目

更新于 10-30 16:30

技能介绍

用途

该技能用于录制移动端应用演示:在 iOS 模拟器上运行 apps/mobile,连接本地技术栈,由 Maestro 驱动,用 simctl 捕获屏幕录像。移动端没有 CDP,因此采用无头启动的 iOS 模拟器替代,不在用户显示器上打开任何窗口。

主要规则

  • 只录制真实应用状态、真实会话、真实页面,不使用 mockup 或 DOM 式伪造;先为故事准备种子数据,录制后删除。
  • 画面中不得出现真实数据:使用开发账号(admin@local.test)和带演示项目的演示组织;分享前逐帧检查联系表,确认姓名、邮箱、仓库名和令牌。
  • 每个故事只拍一次。已连接过的主机不再是“从未连接”,已读页面不再是未读,因此每次拍摄前要重置或重新播种状态。
  • 报告录制对应的 worktree、API 端口、模拟器 UDID 和应用构建日期。

服务启动

各服务在独立的 Superset 终端中启动,端口来自 .superset/ports.json 和根目录 .env。

  • API:cd apps/api && bun run dev。若出现 500 “password authentication failed”,说明该 worktree 的 Neon 分支已被清理,需要重建并改写 .env 中的 “# Workspace Database” 行。
  • Relay:cd apps/relay && while true; do CI=1 WRANGLER_SEND_METRICS=false node_modules/.bin/wrangler dev --port $RELAY_PORT --var NEXT_PUBLIC_API_URL:http://localhost:$API_PORT --local; done。客户端中途中止请求时 wrangler dev 会以空错误退出,而每次应用重启都会触发,因此需要循环。
  • Metro:cd apps/mobile && CI=1 bunx expo start --dev-client --port 8081。CI=1 会关闭文件监听,改代码后需重启 Metro;先用 curl -H 'expo-platform: ios' localhost:8081 获取 launchAsset.url 并预热首个 bundle,否则开发客户端会超时。

apps/mobile/app.config.ts 以 override: true 加载仓库根 .env,因此本地 bundle 始终指向本工作区的 API。

模拟器配置

用 simctl 查找并启动可用设备(示例为 iPhone 17 Pro),设置浅色外观、状态栏覆盖(时间 9:41、电池 100% 放电、信号与 Wi-Fi 等),标记开发菜单引导完成,并通过 openurl 打开开发客户端地址。simctl boot 不会启动 Simulator.app,应保持如此;状态栏覆盖在关机后重置,每次启动后需重新应用。

若未安装开发客户端,或原生依赖变更后启动崩溃,需重新构建:在 apps/mobile 下运行 bunx expo prebuild -p ios,再用 xcodebuild 构建(约 10 分钟),然后 simctl install。设置 SENTRY_DISABLE_AUTO_UPLOAD=true;不要设置 CODE_SIGNING_ALLOWED=NO,否则会丢失钥匙串权限导致首次 SecureStore 读取崩溃。纯 JS 改动无需重建。

用 Maestro 驱动

Maestro 需要 JAVA_HOME=/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home,流程文件位于 apps/mobile/.maestro/flows/,通过 tapOn: { id: dev-sign-in-button } 登录。每个故事写一个流程,优先按可见文本 tapOn,文本有歧义时加 testID;inputText 逐字符输入,在视频中更接近真人打字。Maestro 启动需 7 到 25 秒,应在开始录制前启动,并在流程开头用 waitForAnimationToEnd 或 extendedWaitUntil 让画面稳定。

开发会话的 plan 为 null,首页会显示付费墙;需在工作区 Neon 分支插入 subscriptions 行(plan pro、reference_id 为组织 id、stripe_subscription_id 以 sub_temp_ 开头),录制后删除。

如需一台没有第二台机器的主机,可 POST /api/trpc/host.ensure 并携带会话 JWT 注册,再保持 ws://localhost:$RELAY_PORT/v2/control?hostId=<org>:<machineId> 连接并带 Authorization: Bearer <jwt> 标记在线;之后删除 v2_users_hosts 和 v2_hosts 行。若故事需要真实 agent 工作,则指向真实本地 host-service。

Maestro 匹配整个无障碍标签,行可能带额外文本,应使用正则如 tapOn: ".*Pages.*";可用 maestro --udid $U hierarchy 查看它看到的内容。Metro 若报 ENOENT ... react-native-worklets/.worklets/<n>.js?,说明缓存过期,用 --clear 重启。

Pages 本地附加条件

本地运行 Pages 还需三项 setup 不提供的东西:

  1. 对象存储:API 将页面文件写入 R2_ENDPOINT(localhost:9000,默认无监听),而 usercontent Worker 读取自己的本地 R2。需在 :9000 运行最小 S3 替代服务,通过 page.assets.upload 与 page.publish 发布,再从 apps/usercontent 用 wrangler r2 object put 逐对象复制。
  2. Worker 密钥:apps/usercontent/.dev.vars 需将 USERCONTENT_TOKEN_SECRET 设为根 .env 中的值,否则页面返回 500;用 cd apps/usercontent && bun run dev 启动。
  3. 传输安全:开发构建只允许本地网络,<id>.frame.usercontent.localhost 不算本地,页面会报 -1022。仅对模拟器上已安装的副本,用 PlistBuddy 设置 NSAllowsArbitraryLoads 等项,重启应用,之后还原。

脚本调用 tRPC 需要 authorization: Bearer <jwt> 和 x-superset-organization-id。按 2026-09-19 记录,在模拟器上 Maestro 点击评论未打开评论弹窗,原因未知;可改用“Show all comments”和 Reply 拍摄评论。

录制

在临时目录运行录制命令,仓库外录制时 Maestro 流程用绝对路径。先建目录,再后台执行 xcrun simctl io $U recordVideo --codec h264 --force raw/01-story.mov,运行 maestro test,最后用 kill -INT 停止录制;只能用 SIGINT,其他信号会产生无法播放的文件。

recordVideo 仅在屏幕变化时写帧,文件为可变帧率,墙钟时间不对应视频时间,需先归一化:ffmpeg -i raw/01-story.mov -vf fps=30 -an -c:v libx264 -crf 14 -pix_fmt yuv420p cfr/01.mp4。截图加 sleep 无法捕捉亚秒级闪烁,应录制后用 ffmpeg -vf fps=10 抽帧。更改模拟器变体前,先记录现有外观、内容大小和 AppleLanguages 值以便恢复。变体包括深色外观、超大内容尺寸、切换 AppleLanguages 后重启。若需带边框的演示视频,录制后使用共享的 mobile-demo-film 技能。

清理

只删除本次录制创建的行和对象(订阅、主机、演示会话,以及 Pages 的已发布页面、评论及其在 S3 替代服务和本地 R2 中的对象),还原为演示修改过的组织或用户名。停止本次启动的所有服务,包括 usercontent Worker 和存储替代服务;恢复模拟器原有外观、内容大小和 AppleLanguages(原本未设置则移除覆盖),还原应用传输安全改动,清除状态栏覆盖,最后 xcrun simctl shutdown $U。录像和截图保留在临时目录,除非用户要求放入仓库。