关于 hz-store-pwa
hz-store-pwa 是指导开发者将 Web 应用封装为 Meta VR 应用并提交至 Meta Horizon Store 的技能,涵盖 2D 面板和沉浸式 WebXR 模式。从部署和 PWA 配置到 APK 打包与上传均有说明。
主要功能
- 012D 面板与沉浸式 WebXR 模式指南
- 02Vercel 部署与 PWA 清单配置
- 03使用 @meta-quest/bubblewrap-cli 打包 APK
- 04签名、Digital Asset Links 与商店上传指南
适用场景
- 01准备提交至 Meta Horizon Store 的 Web 应用
- 02选择 2D 或沉浸式应用模式
- 03将已部署的 PWA 打包为签名 APK
- 04排查 TWA 启动和商店上传问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meta-quest/ agentic-tools --skill hz-store-pwa安装后在 Claude Code 中输入/hz-store-pwa确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/app-modes.mdbubblewrap-packaging.mdmanifest-and-icons.mdstore-upload.mdtroubleshooting.mdvercel-deploy.md详细介绍
商店 PWA/TWA 技能
指导将 Web 应用封装为 Meta VR 应用并发布到 Meta Horizon 商店的端到端流程。此技能涵盖两种交付模式——2D 窗口化面板和沉浸式 WebXR/VR——并使用同一套流程:构建 Web 应用、部署到 Vercel、添加 PWA 清单和图标、使用 @meta-quest/bubblewrap-cli 打包为已签名的 Meta VR APK,并通过 ovr-platform-util 上传。
命令使用 <…> 标记(例如 <DOMAIN>、<HORIZON_APP_ID>、<team-slug>、<PW>)——运行前请替换为你自己的值。
何时使用此技能
在以下情况下使用此技能:
- 将 Web 应用(2D 或 WebXR)作为 PWA/TWA 发布到 Meta Horizon 商店
- 决定应用应作为 2D 面板运行,还是作为沉浸式 WebXR 会话运行
- 使用 IWSDK 构建 WebXR 应用,并设置已安装 PWA 的自动进入会话功能
- 将 PWA 部署到 Vercel,并生成有效且可安装的 Web 应用清单和图标
- 使用
@meta-quest/bubblewrap-cli将运行中的 PWA 打包为已签名的 Meta VR APK - 配置签名密钥库和 Digital Asset Links,以便 TWA 能够启动
- 使用
ovr-platform-util将构建版本上传到商店 - 排查 2D 应用卡在加载界面、沉浸式应用显示 URL 栏、TWA 无法启动或上传受阻等问题
如需更深入的 IWSDK 应用构建指南,请参阅 hz-iwsdk-webxr 技能。如需了解更广泛的商店提交流程(VRC 合规、商店素材、审核跟踪),请参阅 hz-store-submit 技能。
流程概览
完整流程按以下顺序进行。两种模式各自不同的步骤已标出;其他步骤对 2D 和沉浸式模式均相同。
0. Pick app mode → 2D panel vs immersive WebXR (sets steps 1 + 4)
1. Build the web app → IWSDK WebXR app (immersive) OR any responsive PWA (2D)
2. Deploy to Vercel → public HTTPS origin = <DOMAIN>
3. Manifest + icons → installable web app manifest, PNG icons, live on <DOMAIN>
4. Package as APK → bubblewrap: keystore, twa-manifest, build, asset links
5. Upload to the Store → ovr-platform-util upload-quest-build
步骤之间的依赖关系很重要——请参阅末尾的操作顺序。
步骤 0:先选择应用模式
应用模式是最重要的单项决策,只需选择一次。它只会改变后续两项内容:
- Web 应用启动时是否自动进入 WebXR 会话(仅限沉浸式模式)。
twa-manifest.json中的horizonOSAppMode值("immersive"或"2D")。
| 2D PWA | 沉浸式 WebXR PWA | |
|---|---|---|
| 运行方式 | 在 Horizon 上以窗口化 2D 面板运行 | 进入完整的 VR/WebXR 会话 |
| Web 应用 | 任意响应式 PWA(IWSDK 可选) | WebXR 应用(IWSDK 是简单的实现路径) |
自动进入 requestSession | 否——不要添加(步骤 1) | 是——内置于应用中(步骤 1) |
horizonOSAppMode | "2D"(步骤 4) | "immersive"(步骤 4) |
horizonOSAppMode 值错误是最常见的故障模式:设为 immersive 的 2D 应用会卡在加载界面;设为 2D 的沉浸式应用会显示浏览器 URL 栏。
完整的决策指南请参阅 references/app-modes.md。
步骤 1:构建 Web 应用
沉浸式 WebXR 应用(IWSDK)
使用 @iwsdk/create 搭建脚手架(唯一受支持的脚手架工具):
npx @iwsdk/create@latest <app-name> --yes --mode vr --no-metaspatial \
--no-physics --no-locomotion --grabbing
根据应用需求启用 --physics(Havok 重力/碰撞)、--locomotion(在大空间中漫游)和 --grabbing(用手或控制器拾取物体)。街机风格的应用建议使用确定性的手动运动,而非物理效果。
不要重新发明 IWSDK 应用代码。 模板附带的 CLAUDE.md、.claude/skills/iwsdk-* 技能以及 iwsdk-rag MCP 是导入、ECS、XR 输入、物理、UI 和调试相关信息的权威来源。请查询这些资源,而不要凭猜测行事。
从一开始就让沉浸式应用支持自动进入会话。 已安装的沉浸式 PWA 启动时不会显示 2D 页面,因此应用本身必须在加载时启动会话(点按应用图标即为用户激活)。通过 getDigitalGoodsService 设置条件,确保此功能仅在已安装的 PWA 中运行,而不会在浏览器标签页中运行:
const nav = navigator as Navigator & { xr?: { isSessionSupported?: (m:string)=>Promise<boolean> } };
if ("getDigitalGoodsService" in window && nav.xr?.isSessionSupported) {
nav.xr.isSessionSupported("immersive-vr")
.then(s => { if (s) world.launchXR(); }) // IWSDK launchXR == requestSession + setup
.catch(() => {});
}
getDigitalGoodsService 仅适用于设备——请在头显上验证此路径。
2D 窗口应用
任何响应式 Web 应用/PWA 都可以,不需要 IWSDK。它会作为独立面板单实例运行,并在库中拥有自己的条目。确保它是有效且可安装的 PWA(步骤 3),并像任何静态/SPA 网站一样构建和部署(步骤 2)。不要添加上方的自动进入代码。
完整的脚手架标志、项目布局和自动进入的原因,见
references/app-modes.md。
步骤 2:部署到 Vercel
打包前,Web 应用必须已在公开 HTTPS 源站上线——bubblewrap
会从中获取清单和图标。在 Vite 配置中设置 base: "./",然后:
npx -y vercel@latest whoami
npx -y vercel@latest teams ls
npx -y vercel@latest deploy --prod --yes --scope <team-slug>
会得到两个 URL:
- 规范别名
https://<project>.vercel.app→ 公开(200)。后续所有步骤中都使用此 URL 作为<DOMAIN>。 - 每次部署生成的哈希 URL
…-<team>.vercel.app→ 在部署保护机制下返回 401。 不要分享,也不能用作<DOMAIN>。
确认根路径和清单均返回 200,且清单以
application/manifest+json 类型提供:
curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/manifest.webmanifest
详情及重新部署与重新构建规则见
references/vercel-deploy.md。
步骤 3:PWA 清单 + 图标
两种模式都需要有效且可安装的清单和 PNG 图标,并且必须在运行
bubblewrap update 前部署到 <DOMAIN>。放置 public/manifest.webmanifest:
{ "name":"…","short_name":"…","description":"…","start_url":"/","scope":"/",
"display":"standalone","orientation":"landscape",
"background_color":"#06010f","theme_color":"#0a0418",
"icons":[
{"src":"/icons/icon-192.png","type":"image/png","sizes":"192x192","purpose":"any"},
{"src":"/icons/icon-512.png","type":"image/png","sizes":"512x512","purpose":"any"},
{"src":"/icons/icon-512-maskable.png","type":"image/png","sizes":"512x512","purpose":"maskable"}]}
在 index.html 的 <head> 中链接清单(<link rel="manifest"> + <meta name="theme-color">
<link rel="icon">)。对于多源 2D 应用,请添加additional_trusted_origins并在每个源上托管资源链接。
这里没有 ImageMagick/PIL——使用 sharp(npm i -D sharp)从 SVG
生成图标。可遮罩图标必须铺满画布且不透明(不能有透明区域或圆角)。
Vite 会将 public/ 复制到 dist/。
图标脚本和完整清单参考见
references/manifest-and-icons.md。
步骤 4:打包为 Meta VR APK(bubblewrap)
bubblewrap 会将线上 PWA 封装为已签名的 Android APK(可信 Web 活动)。
npm i -g @meta-quest/bubblewrap-cli # bin: bubblewrap
所需环境已预先配置在 ~/.bubblewrap 中(其中包含独立的 JDK 17 和 Android SDK)。动态查找工具:
KT=$(find ~/.bubblewrap/jdk -path '*/bin/keytool' | head -1)
BT=$(ls -d ~/.bubblewrap/android_sdk/build-tools/* | sort -V | tail -1)
bubblewrap init 使用需要真实 TTY 的交互式 inquirer 向导。非 TTY 调用方(由 Agent 驱动的 Bash)无法回答向导问题,而且没有可绕过交互的参数——请使用下方的脚本化流程。
-
签名密钥库——构建前暂停并询问开发者要使用哪个密钥。密钥是永久的:今后的每次更新都必须复用它。可以复用现有密钥库(询问其路径、别名和密码——更新已发布的应用时必需),也可以在可部署的 Web 目录之外生成新的密钥库。
-
twa-manifest.json(脚本化流程)——根据权威的TwaManifest架构编写文件。关键字段:"horizonOSAppMode": "immersive" // ← "immersive" for WebXR | "2D" for a 2D panel app
字段值错误是最常见的失败原因(步骤 0)。applicationId = 数字形式的
Horizon App ID("0" 可在不含 IAP 的情况下构建和侧载;进行商店相关工作前请设置真实 ID)。
-
构建——
update会重新生成 gradle 并递增版本;密码通过环境变量传递 (没有密码命令行参数):cd <twa-dir> export BUBBLEWRAP_KEYSTORE_PASSWORD=<PW> BUBBLEWRAP_KEY_PASSWORD=<PW> bubblewrap update && bubblewrap build # → app-release-signed.apk + app-release-bundle.aab "$BT/apksigner" verify --print-certs app-release-signed.apk | grep -i SHA-256 # must == keystore -
数字资产链接——如果失败,TWA 将“无法启动”。在同一域名(以及每个受信任的源)上托管
public/.well-known/assetlinks.json,其中包含包名和冒号分隔的十六进制证书 SHA-256 值。重新部署后, 执行curl https://<DOMAIN>/.well-known/assetlinks.json进行确认。
**安全性:**密钥库和应用密钥绝不能上传到公开主机——使用
curl -o /dev/null -w "%{http_code}" https://<DOMAIN>/android.keystore 验证(预期返回 404)。
请备份密钥库。
完整的密钥库处理说明、twa-manifest.json 完整模板、构建验证和资产链接详情见
references/bubblewrap-packaging.md。
步骤 5:上传到 Meta Horizon Store
hzdb / metavr 仅适用于设备,无法上传。请使用 ovr-platform-util——
同一条命令适用于 2D 和 WebXR 构建。上传前,请在真实头显上检查签名后的 APK(metavr app install,启动应用,快速查看 metavr log)——
Store 审核不是发现启动崩溃的好时机:
./ovr-platform-util upload-quest-build \
--app-id <HORIZON_APP_ID> --app-secret <SECRET> \
--apk app-release-signed.apk \
--channel ALPHA --age-group MIXED_AGES \
--notes "…" --disable-progress-bar
必需参数:--app-id、--apk、--channel、--age-group
(TEENS_AND_ADULTS | MIXED_AGES | CHILDREN),以及 --app-secret 或 --token。
频道:测试使用 ALPHA/BETA/RC,生产使用 STORE。认证凭据为应用的
App Secret(Dashboard → 应用 → API 选项卡)或用户令牌——请向用户询问,切勿自行编造。
首次上传时可能遇到的阻碍:must first agree to our Developer Distribution Agreement——组织管理员必须先在
https://developers.meta.com/horizon/manage/organizations/<ORG_ID>/legal-documents/ 签署一次。暂停操作,
询问用户,然后重试相同的命令。
工具下载、认证和 DDA 阻碍详见 references/store-upload.md。
操作顺序
- 尽早决定使用 2D 还是沉浸式模式(步骤 0)——这会决定自动进入步骤(仅限 WebXR 的步骤 1)和
horizonOSAppMode的值(步骤 4)。 - 在执行
bubblewrap update前,清单和图标必须已上线——该命令会从<DOMAIN>获取它们。 - TWA 启动前,资源链接必须已生效——对每个可信来源都如此。
- 仅修改 Web 内容时,只需重新部署到 Vercel——已安装的 TWA 会在下次启动时获取更改。只有原生部分有改动时(ID、名称、图标、版本、应用模式、打包),才需要重新构建并重新上传 APK。
注意事项
horizonOSAppMode不匹配是头号故障原因——将"2D"设为immersive会卡在加载画面;将"immersive"设为2D会显示 URL 栏。修正该值并重新构建。- 签名密钥是永久的——每次更新都必须重用相同的密钥库、别名和
packageId。密钥库丢失意味着必须新建应用条目。请备份,并将其保存在可部署 Web 目录之外。 bubblewrap init需要 TTY——Agent 必须使用脚本化流程(手动编写twa-manifest.json+update+build)。- 自动进入仅适用于沉浸式模式——切勿将
launchXR()代码片段添加到 2D 应用中,并始终通过getDigitalGoodsService进行门控,避免它在浏览器标签页中触发。 getDigitalGoodsService仅适用于设备——无法在桌面浏览器或模拟器中验证自动进入功能;请在头显上测试。- 使用 Vercel 的规范别名——哈希形式的单次部署 URL 在部署保护下会返回 401,不能用作
<DOMAIN>。 - 可蒙版图标必须铺满画布且不透明——透明部分或圆角会在平台应用蒙版后产生明显瑕疵。
- 资源链接决定应用能否启动——在
/.well-known/assetlinks.json上线且包含匹配的包名和证书 SHA-256 之前,TWA“无法启动”。 - DDA 会阻止首次上传——只有组织管理员可以签署;这是首次上传最常见的失败原因。
参考资料
- 应用模式:2D 与沉浸式 — 决策指南、IWSDK 脚手架、项目布局,以及自动进入会话的设计依据。
- Vercel 部署 — Vite 配置、部署命令、规范 URL 与哈希 URL 的区别,以及重新部署与重新构建的规则。
- PWA 清单与图标 — 完整清单参考、多来源设置,以及
sharp图标生成脚本。 - Bubblewrap 打包 — 密钥库管理、
twa-manifest.json模板、构建验证、资源链接和安全性。 - Store 上传 —
ovr-platform-util下载、认证、上传命令,以及 Developer Distribution Agreement 阻碍。 - 故障排查 — 整个流程中的常见故障模式及修复方法。