Files
Genarrative/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

47 KiB
Raw Blame History

宿主壳能力统一协议

更新时间:2026-06-19

背景

当前主站已经同时运行在普通浏览器、微信小程序 web-view 和后续可能出现的原生 App WebView 中。登录、支付、分享、订阅授权、运行态分享目标同步等能力散落在业务组件和服务文件里,后续如果新增原生 App 壳,容易出现同一业务按宿主重复分叉。

本方案先建立 HostBridge 宿主壳协议,把浏览器、微信小程序壳和未来原生 App 壳统一成能力 adapter。固定玩法运行态仍作为平台内置 runtime,AI 生成 H5 仍走独立 sandbox 和受限 GameBridge;二者都不能直接拿完整宿主壳能力。

目标

  1. H5 业务层只判断宿主能力,不直接散落判断 wx.miniProgramMicroMessengerclientRuntime
  2. 微信小程序壳先作为 wechat_mini_program adapter 接入,保留现有登录、支付、分享、订阅授权行为。
  3. 未来原生 App 壳只新增 native_app adapter,不重写 H5 业务。
  4. 固定玩法继续读取作品数据、素材、运行态 snapshot 和后端裁决结果,不走代码包下载流程。
  5. AI H5 sandbox 只能通过受限 GameBridge 请求资产、上报事件和提交候选结果,不暴露登录、支付、token、完整用户资料。

非目标

  • 不重写 React 主站和现有玩法 runtime。
  • 不把固定玩法迁成远程代码包。
  • 不在 HostBridge 层重写 React Native / Expo 或 Tauri 业务 UI。
  • 不改变支付到账、任务、排行榜、发布、统计等后端裁决口径。

分层

H5 业务层
  -> HostBridge 能力接口
    -> browserHostBridge
    -> wechatMiniProgramHostBridge
    -> nativeAppHostBridge

AI H5 sandbox
  -> GameBridge 受限协议
  -> parent HostBridge adapter

桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:miniprogram/host-bridge/protocol.js 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,dispatch.js 只作为 protocolwebViewpaymentshareGrid 的薄索引,真实协议归一、支付 / 分享结果编解码仍分别放在 webView.jspayment.jsshareGrid.jsminiprogram/shell/webView.jspayment.jsshareGrid.js 承接 Page 生命周期、wx.* 容器调用、WebView 容器行为、支付页装配,页面目录只保留 Page(createWechat...Page()) 装配。Expo 移动壳使用 apps/mobile-shell/src/host-bridge/protocol.ts 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,capabilities.ts 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,dispatch.ts 承接 method 分发和宿主能力调用,appearance.ts 承接系统配色读取,navigation.ts 承接外链打开、受控 H5 跳转和 WebView 刷新,network.ts 承接网络状态查询,badge.ts 承接受控角标能力,clipboard.ts 承接剪贴板读写与 HostBridge payload / 响应边界,files.ts 承接 Expo DocumentPicker / ImagePicker / File / Sharing 系统交互、取消语义、读写编排和 HostBridge 响应包装,filePayloads.ts 承接文件 MIME、大小、base64、文件名清洗和 picker 结果到 HostBridge payload 的边界,share.ts / scanner.ts / notifications.ts 分别承接分享、扫码和本地通知能力,bridge.ts 只作为 WebView message 入口、request id replay 编排和对外 facadeapps/mobile-shell/App.tsx 只装配 apps/mobile-shell/src/shell/ShellApp.tsx,由 apps/mobile-shell/src/shell/*.ts(x) 承接 WebView 容器、URL、导航、网络、生命周期、安全区、扫码 overlay 和 WebView policy。Tauri 桌面壳使用 apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs 承接 envelope、method 白名单、request 校验和 replay 状态,runtime.rs 承接桌面 runtime 回包的平台、hostVersion、bridgeVersion 和 capability 清单组装,appearance.rs 承接窗口主题读取和 HostBridge 配色归一,navigation.rs 承接外链打开、受控 H5 跳转和主窗口刷新,network.rs 承接网络状态查询,badge.rs 承接受控任务栏角标能力,clipboard.rs 承接剪贴板读写与 HostBridge payload / 响应边界,title.rs 承接窗口标题 payload / 响应边界,capabilities.rs 承接共享桌面 capability profile 的 Rust 运行时镜像,dispatch.rs 承接 method 分发和宿主能力调用,files.rs 承接系统文件对话框、取消语义和异步读写编排,file_payloads.rs 承接文件 MIME、大小、base64、文件名清洗、本地副本读写和 HostBridge payload 边界,share.rs / notifications.rs 分别承接分享和本地通知能力,mod.rs 只保留模块声明、必要 re-export、host_bridge_request command facade 和 replay 编排;apps/desktop-shell/src-tauri/src/shell/runtime.rsurl.rsnavigation.rsnetwork.rslifecycle.rsfile_drop.rsevents.rsdeep_link.rstray.rswindow_state.rswebview.rs 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、窗口状态持久化和 WebView 门面,apps/desktop-shell/src-tauri/src/app.rs 承接 Tauri builder / plugin / window 装配,main.rs 只保留薄入口并调用 app::run()npm run check:native-shells 会检查这些目录清单。

当前 npm run check:native-shells 锁定的生产文件清单以本文后续“结构门禁按完整相对路径反查文档和目录”段落为唯一文档口径;不要再维护只含文件名的短清单,避免测试文件、file_payloads.rs 或新增宿主脚本登记发生文档漂移。

npm run check:native-shells 还会按桥接模块语义分类三端结构:dispatchprotocol 必须同时存在于微信、移动和桌面壳;appearancebadgecapabilitiesclipboardfile-payloadsfilesnavigationnetworknotificationsruntimeshare 是 Expo / Tauri 原生 App 壳共同模块;bridgehapticsscanner 只属于移动壳,modtitle 只属于桌面壳,paymentshareGridwebView 只属于微信壳。新增、拆分或迁移 HostBridge 模块时必须先在该分类中明确归属,再同步目录清单和能力流证据。

生产替身词扫描只覆盖上述壳源码、分发配置、共享 HostBridge 契约和已接入真实宿主能力的 H5 调用链;Expo export、Tauri target/、Cargo / Metro 缓存和 release 构建产物不进入扫描范围,避免本地或 CI 生成文件污染源码门禁。

声明为宿主请求能力的 desktop capability 必须在 apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs 命中真实模块委托,不能只由 unsupported_methodunsupported_capability 或 fallback 分支支撑;apps/desktop-shell/scripts/check-config.mjs 负责反查声明能力与委托函数的对应关系。H5 内置玩法如果通过 navigation.openNativePage 打开受控原生壳路由,也必须在 scripts/check-native-shells.mjs 登记 route flow、H5 fallback、路由表和命名交互测试,避免新增内置入口只停留在普通 Web 跳转。

桌面窗口状态持久化只属于 Tauri 宿主壳自身体验,不进入 HostBridge method 或 capability。apps/desktop-shell/src-tauri/src/shell/window_state.rs 必须用 Rust 单测证明只保存大小、位置和最大化状态,并排除可见性、全屏和装饰状态;npm run check:native-shells 会反查该测试边界。

桌面拖拽图片事件只在 Tauri 主窗口拖入真实有效图片时派发 file.imageDropped。目录、文本、损坏图片或没有任何有效图片的拖入不得生成 HostBridge payload,也不得把本地路径暴露给 H5;桌面壳配置检查会反查 file_drop.rs 的无效拖入单测。

结构门禁按完整相对路径反查文档和目录:微信桥接层为 miniprogram/host-bridge/dispatch.jsminiprogram/host-bridge/payment.jsminiprogram/host-bridge/protocol.jsminiprogram/host-bridge/shareGrid.jsminiprogram/host-bridge/webView.js;微信 shell 层为 miniprogram/shell/payment.jsminiprogram/shell/shareGrid.jsminiprogram/shell/webView.js;微信页面包装层为 miniprogram/pages/share-grid/index.jsminiprogram/pages/share-grid/index.jsonminiprogram/pages/share-grid/index.wxmlminiprogram/pages/share-grid/index.wxssminiprogram/pages/web-view/index.jsminiprogram/pages/web-view/index.jsonminiprogram/pages/web-view/index.wxmlminiprogram/pages/web-view/index.wxssminiprogram/pages/wechat-pay/index.jsminiprogram/pages/wechat-pay/index.jsonminiprogram/pages/wechat-pay/index.wxmlminiprogram/pages/wechat-pay/index.wxss;移动源码根为 apps/mobile-shell/src/env.d.ts;移动桥接层为 apps/mobile-shell/src/host-bridge/appearance.test.tsapps/mobile-shell/src/host-bridge/appearance.tsapps/mobile-shell/src/host-bridge/badge.test.tsapps/mobile-shell/src/host-bridge/badge.tsapps/mobile-shell/src/host-bridge/bridge.tsapps/mobile-shell/src/host-bridge/capabilities.test.tsapps/mobile-shell/src/host-bridge/capabilities.tsapps/mobile-shell/src/host-bridge/clipboard.test.tsapps/mobile-shell/src/host-bridge/clipboard.tsapps/mobile-shell/src/host-bridge/dispatch.tsapps/mobile-shell/src/host-bridge/filePayloads.test.tsapps/mobile-shell/src/host-bridge/filePayloads.tsapps/mobile-shell/src/host-bridge/files.test.tsapps/mobile-shell/src/host-bridge/files.tsapps/mobile-shell/src/host-bridge/haptics.test.tsapps/mobile-shell/src/host-bridge/haptics.tsapps/mobile-shell/src/host-bridge/navigation.test.tsapps/mobile-shell/src/host-bridge/navigation.tsapps/mobile-shell/src/host-bridge/network.test.tsapps/mobile-shell/src/host-bridge/network.tsapps/mobile-shell/src/host-bridge/notifications.test.tsapps/mobile-shell/src/host-bridge/notifications.tsapps/mobile-shell/src/host-bridge/protocol.test.tsapps/mobile-shell/src/host-bridge/protocol.tsapps/mobile-shell/src/host-bridge/runtime.test.tsapps/mobile-shell/src/host-bridge/runtime.tsapps/mobile-shell/src/host-bridge/scanner.test.tsapps/mobile-shell/src/host-bridge/scanner.tsapps/mobile-shell/src/host-bridge/share.test.tsapps/mobile-shell/src/host-bridge/share.ts;移动 shell 层为 apps/mobile-shell/src/shell/QrScannerOverlay.test.tsxapps/mobile-shell/src/shell/QrScannerOverlay.tsxapps/mobile-shell/src/shell/ShellApp.tsxapps/mobile-shell/src/shell/deepLink.tsapps/mobile-shell/src/shell/lifecycle.tsapps/mobile-shell/src/shell/loadFailure.tsapps/mobile-shell/src/shell/navigation.tsapps/mobile-shell/src/shell/network.tsapps/mobile-shell/src/shell/runtime.tsapps/mobile-shell/src/shell/safeArea.tsapps/mobile-shell/src/shell/url.tsapps/mobile-shell/src/shell/webViewGlobals.d.tsapps/mobile-shell/src/shell/webViewHistory.tsapps/mobile-shell/src/shell/webViewPolicy.ts;桌面入口为 apps/desktop-shell/src-tauri/src/app.rsapps/desktop-shell/src-tauri/src/main.rs;桌面桥接层为 apps/desktop-shell/src-tauri/src/host_bridge/appearance.rsapps/desktop-shell/src-tauri/src/host_bridge/badge.rsapps/desktop-shell/src-tauri/src/host_bridge/capabilities.rsapps/desktop-shell/src-tauri/src/host_bridge/clipboard.rsapps/desktop-shell/src-tauri/src/host_bridge/dispatch.rsapps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rsapps/desktop-shell/src-tauri/src/host_bridge/files.rsapps/desktop-shell/src-tauri/src/host_bridge/mod.rsapps/desktop-shell/src-tauri/src/host_bridge/navigation.rsapps/desktop-shell/src-tauri/src/host_bridge/network.rsapps/desktop-shell/src-tauri/src/host_bridge/notifications.rsapps/desktop-shell/src-tauri/src/host_bridge/protocol.rsapps/desktop-shell/src-tauri/src/host_bridge/runtime.rsapps/desktop-shell/src-tauri/src/host_bridge/share.rsapps/desktop-shell/src-tauri/src/host_bridge/title.rs;桌面 shell 层为 apps/desktop-shell/src-tauri/src/shell/deep_link.rsapps/desktop-shell/src-tauri/src/shell/events.rsapps/desktop-shell/src-tauri/src/shell/file_drop.rsapps/desktop-shell/src-tauri/src/shell/lifecycle.rsapps/desktop-shell/src-tauri/src/shell/mod.rsapps/desktop-shell/src-tauri/src/shell/navigation.rsapps/desktop-shell/src-tauri/src/shell/network.rsapps/desktop-shell/src-tauri/src/shell/runtime.rsapps/desktop-shell/src-tauri/src/shell/tray.rsapps/desktop-shell/src-tauri/src/shell/url.rsapps/desktop-shell/src-tauri/src/shell/webview.rsapps/desktop-shell/src-tauri/src/shell/window_state.rs。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。

移动端结构门禁同步覆盖测试文件完整相对路径:apps/mobile-shell/src/host-bridge/bridge.test.tsapps/mobile-shell/src/host-bridge/dispatch.test.tsapps/mobile-shell/src/shell/ShellApp.test.tsxapps/mobile-shell/src/shell/deepLink.test.tsapps/mobile-shell/src/shell/lifecycle.test.tsapps/mobile-shell/src/shell/loadFailure.test.tsapps/mobile-shell/src/shell/navigation.test.tsapps/mobile-shell/src/shell/network.test.tsapps/mobile-shell/src/shell/runtime.test.tsapps/mobile-shell/src/shell/safeArea.test.tsapps/mobile-shell/src/shell/url.test.tsapps/mobile-shell/src/shell/webViewHistory.test.tsapps/mobile-shell/src/shell/webViewPolicy.test.ts

Tauri 桌面壳启动时必须按 label="main" 解析 tauri.conf.json 主窗口配置,并在创建 WebView 前补写 native_apptauri_desktop 和真实 capability 上下文;缺少主窗口配置时启动直接失败,不允许按 windows[0] 兜底或无主窗口静默运行。

宿主上下文 query 的字段名和值以 packages/shared/src/contracts/hostBridge.ts 为源。HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY 覆盖 H5 runtime 识别可读取的 clientRuntimeclientTypeminiProgramEnvhostShellhostPlatformhostVersionbridgeVersionhostCapabilitiesHOST_BRIDGE_NATIVE_APP_QUERY_KEYHOST_BRIDGE_NATIVE_APP_QUERY_KEYSHOST_BRIDGE_NATIVE_APP_QUERY 固定 Expo / Tauri 原生壳入口 queryHOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY 固定微信 WebView 来源标记;HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS 固定 H5 页面内导航需要跨路径保留的宿主字段,必须同时覆盖微信小程序来源字段和原生壳 hostShellhostPlatformhostVersionbridgeVersionhostCapabilities 完整运行态字段。Expo 移动壳直接引用共享常量,Tauri Rust 和微信小程序 CommonJS 运行时镜像由 npm run check:native-shells 反查;H5 getHostRuntime() 和路由保留列表不得重新手写这些字段。

移动壳 WebView 下载协议阻断清单以 packages/shared/src/contracts/hostBridge.tsHOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS 为源。Expo 壳导航拦截和 WebView 注入脚本必须复用同一清单,命中后直接拒绝进入带完整 HostBridge 的 WebView;移动端文件保存只通过受控 file.exportTextfile.exportImagefile.exportAudio 能力进入系统分享 / 保存面板。

首批能力

  • getHostRuntime():识别 browserwechat_mini_programnative_app,并解析 hostCapabilities 能力声明;进入 native_app 后会通过真实 host.getRuntime 回读宿主 runtime 并缓存能力清单,未知能力会被丢弃。H5 业务只根据已声明或已回读的能力展示入口、发起宿主请求或走 fallback。
  • getHostAppearanceColorScheme():原生 App 宿主的受控外观查询入口。H5 可通过 appearance.getColorScheme 读取宿主当前 light / dark / unknown 配色模式;Expo 移动壳通过 React Native Appearance.getColorScheme() 读取系统偏好,Tauri 桌面壳通过主窗口 theme() 读取窗口主题。该能力只读,不改变 H5 主题,也不覆盖用户或系统偏好。
  • subscribeHostAppLifecycle():原生 App 宿主的受控生命周期事件入口。Expo 移动壳和 Tauri 桌面壳都声明 host.events,表示宿主会通过 HostBridge message 派发事件;其中 Expo 移动壳通过 React Native AppState 派发 app.lifecycle,Tauri 桌面壳通过主窗口 focus / blur、托盘隐藏 / 恢复和页面加载重放派发同名事件。桌面壳不会把 hidden、minimized 或 tray 扩成新的 state,而是读取 is_visible()is_minimized()is_focused() 后统一归一为 active / inactive / background,并只把 hiddenminimizedfocusedblurred 放进 nativeState 用于排障。host.events 不作为 request method,也不开放 Tauri event 插件或 React Native 私有事件 API。H5 只依赖统一的 active / inactive / background 状态和 focused 布尔值,原生细分状态只放在 nativeState 用于排障,不作为业务分支依据。H5 统一通过 useHostLifecycleActive() 把宿主状态折算为运行态可播放状态;WebAudio 背景音乐和固定玩法 <audio> 背景音乐都必须按该状态暂停 / 恢复,宿主进入后台、inactive 或窗口失焦时暂停,回到 active 且 focused 后只在原运行态、音源和用户音量仍允许时恢复。
  • getHostNetworkStatus() / subscribeHostNetworkStatusChange():原生 App 宿主的受控网络状态入口。Expo 移动壳通过 expo-network 查询并订阅真实系统网络状态;Tauri 桌面壳从 WEB_APP_ORIGIN 解析主站 host / port 后按共享契约 HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS 做短超时 TCP 可达性查询,并在主 WebView 内监听 online / offline 注入变化事件。H5 只依赖统一的 isConnectedisInternetReachable 和连接类型,不直接读取平台私有网络 API。平台外部生成队列概览通过 useHostNetworkOnline() 消费该状态,宿主未声明网络能力时保持原轮询行为,宿主明确离线或不可达时暂停轮询,恢复在线后重新刷新;该状态不替代后端队列事实或生成结果回读。
  • subscribeHostNavigationCanGoBack() / useHostNavigationCanGoBack():原生 App 宿主的受控返回栈状态入口。H5 只有在宿主同时声明 host.eventsnavigation.canGoBack 时才订阅该事件;Expo 移动壳和 Tauri 桌面壳注入的状态只表示当前 H5 文档路由栈或宿主已归一后的返回状态,不让 H5 读取任意原生 back-forward list。主 App 在原生壳内直达非平台首页、非 runtime 的二级 H5 route 且当前 history state 没有应用导航标记时,会先把当前条目替换为 / 返回锚点,再把当前路径连同已保留的宿主 query 推回 history;普通浏览器、小程序、runtime 路由、已有应用 history 或不支持返回栈事件的裁剪壳不触发该补齐。 声明 host.eventsapp.lifecyclenetwork.statusChangednavigation.canGoBackfile.imageDropped 后,宿主壳必须把对应 WebView 事件脚本注册或发射失败显式失败或记录日志;启动阶段无法安装网络 / 返回栈事件时不得继续以半可用状态运行,页面重放、窗口生命周期事件和拖拽图片事件阶段不得静默吞掉失败。
  • requestHostLogin():微信小程序跳转原生登录页;浏览器返回 false,由 H5 登录弹窗承接。
  • requestHostPayment():微信小程序支付跳转原生支付页;其它渠道返回 false,继续走 H5 / Native 二维码。
  • setHostShareTarget():把当前公开作品分享目标同步给宿主。
  • openHostShare():原生 App 宿主的受控分享入口。发布分享弹窗只在 hostCapabilities 声明 share.open 时展示宿主分享动作,通过 share.open 把当前作品标题、作品号和公开 URL 交给宿主;Expo 移动壳打开系统分享面板,H5 展示“系统分享 / 已打开 / 分享失败”;Tauri 桌面壳把分享文本写入系统剪贴板,H5 展示“复制分享文案 / 已复制 / 复制失败”,不得把桌面剪贴板动作包装成系统分享面板。H5 facade 和 Expo 移动壳都通过共享契约 normalizeHostBridgeShareOpenPayload()urlhrefpathtargetPathwork 归一到 https://www.genarrative.world 同源公开 URL,Tauri 桌面壳用 Rust 镜像同一规则;外域、协议相对 URL、危险协议和无法归一的显式分享目标必须返回 invalid_request,且不得回退到之前缓存的 share.setTarget 目标;宿主不可用或返回 unsupported 时显示失败并保留复制链接路径。
  • openHostShareGrid():微信小程序九宫格切图页。
  • writeHostClipboardText():原生 App 宿主的受控剪贴板入口。H5 复制服务在 native_app 中优先通过 clipboard.writeText 写入 Expo / Tauri 系统剪贴板;H5 facade 发起请求前先按共享契约 HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH 归一化 payload,减少 WebView bridge 承载超长文本;两端壳写入前也必须执行同一上限截断,不能让 H5 透传超长剪贴板内容;宿主不可用、拒绝或返回 unsupported 时继续回退到浏览器 Clipboard API 和 legacy selection copy。
  • readHostClipboardText():原生 App 宿主的受控剪贴板读取入口。H5 只能读取纯文本结果,宿主返回内容会按 HostBridge 契约限制到 100000 字符;Expo 移动壳通过 expo-clipboard 读取系统剪贴板文本,Tauri 桌面壳通过 Rust 侧 clipboard-manager 读取系统剪贴板文本。该能力不读取图片、HTML、文件列表或剪贴板监听事件,不把 Tauri / Expo 剪贴板插件 API 直接暴露给 H5;宿主未声明或读取失败时由 H5 视作失败并保留原流程。个人中心的邀请码和兑换码弹窗只在宿主声明 clipboard.readText 时显示“粘贴”,读取到的纯文本只填入现有输入框,不自动提交、不代表兑换成功。
  • requestHostHapticsImpact():原生 App 宿主的受控触觉反馈入口。Expo 移动壳通过 haptics.impact 调用 Expo Haptics,只接受 lightmediumheavy 三档 impact style,缺省为 light;H5 facade 发起请求前先按共享清单归一化 style,未知值不发往宿主,移动壳仍必须二次拒绝未知值且不触发设备反馈;H5 运行时点击反馈在 native_app 中优先请求宿主触觉,宿主不可用、拒绝或返回 unsupported 时继续回退到浏览器 navigator.vibrate。 移动壳的触觉反馈 payload 解析、style 归一、Expo style 映射和真实设备反馈调用都必须留在 apps/mobile-shell/src/host-bridge/haptics.tsdispatch.ts 只把 request.payload 委托给该模块。
  • showHostLocalNotification():原生 App 宿主的受控即时本地通知入口。H5 只能传必填 title 和可选 body,两者都会去除首尾空白、折叠普通空白、限制长度并拒绝控制字符;Expo 移动壳通过 expo-notifications 请求通知权限、创建 Android 本地通知 channel 并立刻调度本地通知,Android channel id 固定为共享契约 HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_IDTauri 桌面壳通过 Rust 侧 tauri-plugin-notification 先检查系统通知权限,处于 prompt 状态时只在 Rust 侧请求一次权限,最终授权后才发送系统通知。成功结果统一为共享契约 HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_RESULT,只表示通知已交给系统通知层,不承诺用户一定看见或点击。该能力不包含远程推送、token 注册、定时提醒、后台远程通知或任意通知插件透传,宿主未声明、权限拒绝或系统失败时由 H5 视作失败并继续主流程。当前 H5 只在现有草稿生成任务收口为完成或失败时请求即时本地通知;通知按草稿来源去重,同一草稿重新进入生成中后才允许再次通知,不改变队列状态、弹窗、作品架或后端裁决。平台壳同步层必须通过真实 host_bridge_request transport 测到 notification.showLocal 请求,不能只测模型文案或替换 facade。
  • setHostAppTitle():原生 App 宿主的受控窗口标题入口。H5 主站会按当前平台阶段先同步 document.title,再通过 app.setTitle 请求宿主窗口标题同步;H5 facade 和 Tauri 桌面壳都必须按共享契约 HOST_BRIDGE_APP_TITLE_MAX_LENGTH 清洗标题,拒绝空值和控制字符,最多保留 80 个字符。Tauri 桌面壳支持该能力,Expo 移动壳不声明时静默忽略。
  • setHostAppBadgeCount():原生 App 宿主的受控应用角标入口。H5 只传 0 到共享契约 HOST_BRIDGE_BADGE_COUNT_MAX 之间的整数,0 表示清除角标;Expo 移动壳只在 iOS 声明 app.setBadgeCount,并通过 Expo Notifications 查询 / 请求 allowBadge 权限,再以 Notifications.setBadgeCountAsync(count) 的返回值作为成功依据;权限拒绝、权限状态不可读、系统返回 false 或原生 API 异常都必须返回明确失败,Android 不声明该能力,且请求实际到达 Android 壳时必须返回 unsupported_capability,不得返回成功;Tauri 桌面壳通过主窗口 set_badge_count 设置任务栏角标,底层平台不支持时返回明确错误,由 H5 视作失败并继续主流程。当前 H5 只把“可见作品架里未读的草稿生成完成更新”同步为角标数,同一个草稿有多个恢复 ID 时只计 1,已读、失败、生成中和不可见草稿不计入;宿主不支持或设置失败不改变 H5 红点、作品架或后端状态。平台壳同步层必须通过真实 host_bridge_request transport 测到 app.setBadgeCount 请求,保证未读计数模型和原生壳消费链路同时被门禁覆盖。
  • reloadHostWebView():原生 App 宿主的受控 WebView 刷新入口。H5 只能请求刷新当前承载主站的宿主 WebView;Expo 移动壳调用当前 react-native-webviewreload()Tauri 桌面壳调用主 WebviewWindow.reload()。该能力不接受 payload,不开放任意 URL 导航、脚本执行、Tauri guest API 或 RN WebView ref;成功只表示宿主已发起刷新,刷新后当前 H5 上下文会卸载。AuthGate 在登录态从未登录变为已登录、或从已登录变为未登录时优先调用该能力刷新当前容器;宿主未声明、返回失败或不可用时再回退浏览器 window.location.reload()
  • openHostExternalUrl():原生 App 宿主的受控外链入口。H5 中需要离开主站的外链在 native_app 下先通过 app.openExternalUrl 请求宿主系统浏览器打开;只允许 http:https:mailto:tel:,相对路径会先归一化到当前站点绝对 URL,再通过共享契约 normalizeHostBridgeExternalUrlPayload() 清洗为 { url } 载荷。Expo 移动壳消费该共享 payload normalizerTauri 桌面壳在 Rust 侧用 URL parser 镜像同一协议清单。宿主不可用或拒绝时回退浏览器外链行为,普通浏览器和小程序保持原有 <a> 语义。H5 支付链接和微信 OAuth 登录授权 URL 也走该入口:原生壳未声明真实 payment.request / auth.requestLogin 前,微信 H5 支付 URL 和后端返回的微信登录授权 URL 优先交给宿主系统浏览器,宿主未处理时才回退当前 WebView 跳转;不得把 H5 支付或网页登录伪装成已完成的原生支付 / 原生登录。
  • navigateHostNativePage():受控跳转宿主页,供订阅授权、支付、登录和仍在维护的同源 H5 adapter 复用。Expo 移动壳首版只接受同源 H5 route 并切换 WebView URLTauri 桌面壳同样只接受 https://www.genarrative.world 同源 H5 route 并在主窗口内跳转。H5 facade 在 native_app 下发送 navigation.openNativePage 前先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 http: / https: 协议目标,避免把明显不安全的跳转请求交给原生壳;同源绝对 URL、/path 和保留给桌面壳兼容的相对 route 继续由宿主二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 wx.miniProgram.navigateTo,不套原生 App 同源 H5 预校验。旧创作入口和儿童动作 Demo 已退出宿主壳强制验收,不再作为 check:native-shells 的专属路由契约;真正原生页面、登录和支付能力必须等对应 SDK / 页面接入后再声明支持。
  • exportHostTextFile():原生 App 宿主的受控文本导出入口。H5 facade 发起请求前先通过共享契约 normalizeHostBridgeExportTextPayload() 预校验文件名、文本内容、可选 MIME 和 5 MiB 上限;Expo 移动壳通过 file.exportText 写入缓存文本文件并交给系统分享 / 保存面板;Tauri 桌面壳通过 file.exportText 打开系统保存对话框并写入用户选择的文件。文件名必须清洗,可选 MIME 只能来自共享契约 HOST_BRIDGE_TEXT_MIME_TYPES,未传时默认为 text/plain,非文本 MIME 必须拒绝,不能借文本导出通道伪装成图片、音频或二进制文件;Expo 与 Tauri 壳仍必须二次校验真实文本字节数和 MIME。成功只返回文件名和字节数,不把本机绝对路径暴露给 H5;系统分享不可用或用户取消时返回明确错误,由 H5 fallback 承接。创作 Agent 工作台在 native_app 且声明该能力时提供会话 Markdown 导出入口,导出内容只来自当前 H5 已持有的会话标题、摘要、进度、锚点、消息、流式回复和输入草稿,并在 H5 侧先按同一 5 MiB 上限做 UTF-8 byte 校验;普通浏览器、小程序和未声明能力的裁剪壳不展示该入口。
  • importHostTextFile():原生 App 宿主的受控文本导入入口。Expo 移动壳通过 Expo DocumentPicker 打开系统文档选择器,Tauri 桌面壳通过系统文件选择框读取用户选择的文本文件;两端都只接受 text/plaintext/markdowntext/csvapplication/json 或对应扩展名,单次不超过 5 MiB,成功只返回通过共享 normalizeHostBridgeImportFileName() 清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备本地 URI 或本机绝对路径,也不开放通用文件系统能力;宿主必须在读取文本内容前拿到可信 byte count 并完成上限校验,移动壳在 picker 缺少 size 时改用 Expo File.size,仍拿不到可信大小时直接拒绝导入;H5 facade 收到结果后继续通过共享契约 normalizeHostBridgeImportTextResult() 复核文件名、MIME、文本内容和字节数,非法或超界结果归为 false;用户取消时由 H5 facade 归为 false。创作 Agent 工作台在 native_app 且声明该能力时优先调用宿主文本导入,并把结果转换成现有浏览器 File 后继续复用后端 /api/runtime/creation-agent/document-inputs/parse 解析链路;普通浏览器、小程序和未声明能力的裁剪壳继续使用原文件输入。
  • importHostDocumentFile():原生 App 宿主的受控文档导入入口。Expo 移动壳通过 Expo DocumentPickerTauri 桌面壳通过系统文件选择框读取用户选择的文档副本;两端都只接受 text/plaintext/markdowntext/csvapplication/jsonapplication/vnd.openxmlformats-officedocument.wordprocessingml.document 或对应 .txt / .md / .markdown / .csv / .json / .docx 扩展名,单次不超过 5 MiB。成功只返回通过共享 normalizeHostBridgeImportFileName() 清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI、本机绝对路径或通用文件系统能力;宿主必须在读取 base64 前拿到可信 byte count 并完成上限校验,移动壳在 picker 缺少 size 时改用 Expo File.size,仍拿不到可信大小时直接拒绝导入;H5 facade 收到结果后继续通过共享契约 normalizeHostBridgeImportDocumentResult() 复核文件名、MIME、base64 和字节数。创作 Agent 工作台在 native_app 且声明该能力时优先调用宿主文档导入,把返回 base64 转换成现有浏览器 File 后继续调用 /api/runtime/creation-agent/document-inputs/parse;旧壳只声明 file.importText 时才回退到文本导入,普通浏览器、小程序和未声明能力的裁剪壳继续使用原文件输入。该能力不在前端解析 DOCX,也不绕过后端文档解析、大小校验或错误口径;移动壳配置检查必须强制文档 MIME 清单、5 MiB 上限和导入文件名清洗函数来自共享 HostBridge 契约。
  • exportHostImageFile():原生 App 宿主的受控图片导出入口。H5 只传自己生成的图片 base64Data、清洗后的文件名和允许的 image/png / image/jpeg / image/webp MIMEH5 facade 发起请求前先通过共享契约 normalizeHostBridgeExportImagePayload() 预校验文件名、MIME、base64 和 5 MiB 上限,Expo 与 Tauri 壳仍必须二次校验真实字节与 MIME。Expo 移动壳写入缓存图片后交给系统分享 / 保存面板,Tauri 桌面壳打开系统保存对话框并写入图片字节。成功只返回文件名和字节数,不回传本机绝对路径。当前分享卡下载在 native app 中优先走 file.exportImage,宿主未声明时保留浏览器下载路径。
  • importHostImageFile() / captureHostImageFile() / subscribeHostImageDrop():原生 App 宿主的受控图片导入入口。Expo 移动壳通过 Expo ImagePicker 请求相册权限并打开系统相册选择器,也可在声明 file.captureImage 时请求相机权限并打开系统相机拍摄图片;Tauri 壳通过系统文件选择框或主窗口拖拽事件读取用户选择 / 拖入的图片,不声明拍摄能力。图片能力都只接受 image/pngimage/jpegimage/webp,单次不超过 10 MiB,成功只返回文件名、MIME、base64 内容、字节数和可选拖入坐标,不暴露设备本地 URI 或本机绝对路径,也不开放通用文件系统能力;移动拍摄能力不使用麦克风,移动壳包级麦克风权限只服务同源 H5 实时声音玩法。H5 facade 收到导入、拍摄或拖拽结果后继续通过共享契约 normalizeHostBridgeImportImageResult() 复核文件名、MIME、base64、字节数和可选坐标。H5 的通用图片输入面板 CreativeImageInputPanelnative_app 且声明 file.importImage / file.captureImage 时分别调用宿主导入 / 拍摄,并把结果转换成现有 File 回调;创作 Agent 工作台参考图上传、轻输入 composer 参考图上传、反馈页上传凭证、个人资料头像上传、方洞结果页图片槽位上传、汪汪声浪结果页三图槽位上传、抓大鹅结果页发布封面 / 封面参考图上传、RPG 角色资产工作室角色参考图上传、RPG 作品封面 / 封面参考图上传、RPG 场景图片参考图上传和视觉小说结果页图片素材上传在 native_app 且声明 file.importImage 时同样优先调用宿主图片导入,其中创作 Agent 工作台继续把宿主返回内容转换成现有浏览器 File 后交给 onReferenceImageChange 校验链路,轻输入 composer 继续复用 readPuzzleReferenceImageAsDataUrl 的图片类型、大小、压缩和 data URL 预览链路,反馈页继续复用原有数量、大小、data URL 和提交 payload 校验,头像继续复用 H5 侧图片类型、5 MiB 大小限制、方形裁剪与 updateAuthProfile 上传链路,方洞结果页继续把图片内容写回当前封面 / 背景 / 形状 / 洞口槽位并走现有自动保存和发布链路,汪汪声浪结果页继续把图片转换成浏览器 File 后交给 uploadBarkBattleAsset 上传和槽位写回链路,抓大鹅结果页继续复用现有封面 data URL 读取、AI 重绘开关、参考图集合和封面生成 payload 链路,RPG 角色资产工作室继续复用现有 readFileAsDataUrl 参考图集合和角色形象生成 payload 链路,RPG 作品封面上传继续复用现有 10 MiB 校验、图片尺寸读取、16:9 裁剪和 uploadCustomWorldCoverImage 保存链路,RPG 作品封面参考图继续复用现有 readImageFileAsDataUrl 读取、预览和 generateCustomWorldCoverImage payload 链路,RPG 场景图片参考图继续复用现有 readImageFileAsDataUrl 读取、预览和 rpgCreationAssetClient.generateSceneImage payload 链路,视觉小说结果页继续把图片转换成浏览器 File 后交给 uploadVisualNovelAsset 上传和当前封面 / 角色 / 场景素材写回链路;反馈页和轻输入 composer 在移动壳声明 file.captureImage 时额外展示拍摄入口,并把拍摄结果复用同一图片校验与提交链路。在桌面壳同时声明 file.imageDropped 时,只有拖入坐标命中当前主图卡片且未被上层元素遮挡的面板会消费该事件。普通浏览器、小程序和未声明能力的裁剪壳继续使用浏览器文件输入。
  • scanHostQrCode():原生 App 宿主的受控二维码扫描入口。Expo 移动壳声明 scanner.scanQrCode,通过 expo-camera 的真实相机权限和 CameraView 扫描 QR code,成功只返回清洗后的二维码文本与 qr_code 格式,单次值最多保留 4096 字符且拒绝空值和控制字符;H5 facade 的扫码等待上限固定读取共享契约 HOST_BRIDGE_SCANNER_TIMEOUT_MS,不得在业务调用点手写毫秒数;用户关闭或系统取消返回 cancelled,H5 不会继续连带弹出浏览器摄像头权限。Tauri 桌面壳只把 scanner.scanQrCode 保留在 method 白名单中用于明确返回 unsupported_method,不声明 capability、不伪造桌面扫码。个人中心扫码入口在 native_app 且宿主声明该能力时优先调用原生扫码;宿主不支持、旧壳缺能力或扫码结果非法时继续打开现有浏览器摄像头扫码弹层,普通浏览器和小程序保持原有路径。

HostBridge 事件名以 packages/shared/src/contracts/hostBridge.tsHOST_BRIDGE_EVENTS 为唯一白名单,当前为 app.lifecyclenetwork.statusChangednavigation.canGoBackfile.imageDropped;事件名必须存在于 capability 白名单,但各宿主壳只声明自身真实发射的事件能力。Expo 壳事件注入使用共享 HostBridgeEventName 类型,Tauri 壳 shell/events.rs 镜像同一清单并拒绝未知事件,H5 nativeAppHostBridge 只分发共享白名单内事件。H5 事件订阅入口必须同时要求 host.events 和对应事件 capability,不能仅凭 app.lifecyclenetwork.statusChangednavigation.canGoBackfile.imageDropped 单项能力就绑定事件监听;旧壳或裁剪壳缺任一能力时订阅应返回空取消函数。npm run check:native-shells 会反查共享事件清单、H5 订阅 facade 和 canUseNativeHostEventCapability(...),防止后续事件订阅绕过双能力门控。

  • importHostAudioFile():原生 App 宿主的受控音频导入入口。Expo 移动壳通过 Expo DocumentPicker 打开系统音频选择器,Tauri 壳通过系统文件选择框读取用户选择的音频;两端都只接受 audio/mpegaudio/mp4audio/wavaudio/oggaudio/webm 或对应扩展名,单次不超过 20 MiB,成功只返回清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI 或本机绝对路径,也不开放通用文件系统能力;宿主必须在读取音频内容或生成 base64 前拿到可信 byte count 并完成上限校验,移动壳在 picker 缺少 size 时改用 Expo File.size,仍拿不到可信大小时直接拒绝导入;H5 facade 收到结果后继续通过共享契约 normalizeHostBridgeImportAudioResult() 复核文件名、MIME、base64 和字节数。H5 的通用音频输入面板 CreativeAudioInputPanelnative_app 且声明 file.importAudio 时优先调用宿主导入,并把结果转换成现有 File 后继续复用 readFileAsAsset(file, 'uploaded') 音频处理链路;视觉小说结果页音乐和环境音素材上传同样优先调用宿主音频导入,再把返回副本转换成浏览器 File 后继续交给 uploadVisualNovelAsset 上传和场景音频字段写回链路。普通浏览器、小程序和未声明能力的裁剪壳继续使用浏览器文件输入。
  • exportHostAudioFile():原生 App 宿主的受控音频导出入口。H5 只传当前页面已持有的音频 base64Data、清洗后的文件名和允许的 audio/mpeg / audio/mp4 / audio/wav / audio/ogg / audio/webm MIMEH5 facade 发起请求前先通过共享契约 normalizeHostBridgeExportAudioPayload() 预校验文件名、MIME、base64 和 20 MiB 上限,Expo 与 Tauri 壳仍必须二次校验真实字节与 MIME。Expo 移动壳写入缓存音频后交给系统分享 / 保存面板,Tauri 壳打开系统保存对话框并写入音频字节。成功只返回文件名和字节数,不回传本机绝对路径,也不让宿主代读任意本地文件。H5 的通用音频输入面板只在当前资产包含本地 BlobfileName 和允许 MIME 且宿主声明 file.exportAudio 时展示导出入口;远端已上传音频、浏览器、小程序和未声明能力的裁剪壳不展示该入口。

Tauri 桌面壳的文件能力边界分为两层:apps/desktop-shell/src-tauri/src/host_bridge/files.rs 统一持有系统文件对话框过滤器、用户取消语义、路径转换、异步读写和 HostBridge 响应归一,apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs 统一持有文件 payload 校验、MIME / 大小 / bytes 边界、文件名清洗、本地副本读写和导入导出 payload 组装;dispatch.rs 只按 method 委托文件模块,不直接调用 .dialog()blocking_save_file / blocking_pick_file、文件 payload helper 或落盘 helper。

迁移顺序

  1. 新增 src/services/host-bridge/,沉淀宿主运行态识别和微信小程序 JS SDK 加载,并暴露通用 HostBridge 能力接口。
  2. authService 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。
  3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。
  4. 后续新增 native_app adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 host.getRuntime 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 hostCapabilities 时长期隐藏真实可用能力。
  5. 每次新增或调整 native capability、HostBridge event 或宿主上下文 query 后,必须先更新 packages/shared/src/contracts/hostBridge.ts 中对应微信 / Expo / Tauri capability profile、事件白名单和 query 契约,再运行 npm run check:native-shells,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release --no-bundle 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。移动壳 EAS build config smoke 必须确认 Android 生产 profile 能产出内部 APKiOS 生产 smoke profile 只产出 simulator 包,且不写商店提交、签名凭据来源、OTA channel 或本机 dotenv;移动壳 Metro export smoke 必须读取 iOS / Android production bundle,确认最终 bundle 使用共享 HostBridge 契约中的生产 H5 URL,且没有混入本机开发 H5 URL。移动壳和桌面壳文档中的主状态段落能力清单与完整能力清单都必须反查共享 capability profile,避免同一文档内部漂移;桌面壳 host_bridge_request command facade 必须先做 request 校验再进入 replay / 分发,且单测覆盖非法 envelope 不占用 replay slot;桌面壳 capabilities.rs 的 Rust 单测必须同时覆盖能力清单顺序、无重复、真实桌面能力完整包含和未接入能力排除;桌面壳未声明的共享 request method 必须由 Rust 测试从 HOST_BRIDGE_METHODS - capabilities() 自动派生为 unsupported_method 覆盖清单,当前包括 auth.requestLoginpayment.requestfile.captureImagescanner.scanQrCodehaptics.impact,不得伪造成功;Expo 移动壳未声明的共享 request method 必须由 HOST_BRIDGE_METHODS - HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES 自动派生测试覆盖,确保未接 SDK / 渠道的 method 返回明确 unsupported_methodH5 facade 除 host.getRuntime 真实回读外,所有 native_app request 能力都必须通过 canUseNativeHostCapability(...) 统一门控,根级门禁会从共享 HOST_BRIDGE_METHODS 自动派生需检查清单;排查单端问题时再单独运行 npm run mobile-shell:typechecknpm run mobile-shell:testnpm run mobile-shell:build-confignpm run mobile-shell:confignpm run mobile-shell:exportnpm run desktop-shell:typechecknpm run desktop-shell:testnpm run desktop-shell:build -- --no-bundle

验收

  • 微信小程序首点登录仍能打开原生登录页。
  • 小程序分享链接仍生成 /pages/web-view/index?targetPath=/works/detail&work=...
  • 小程序支付仍跳转 /pages/wechat-pay/index 并保留支付结果 hash 回灌确认。
  • 旧玩法生成结果订阅授权页已退出小程序 app.json 和现役 HostBridge 能力清单,历史源码移入退役目录。
  • 普通浏览器分享、H5 支付和 Native 二维码支付不受影响。
  • 原生壳统一验收入口 npm run check:native-shells 通过,能力白名单、微信 / Expo / Tauri 共享 capability profile、HostBridge event 白名单、宿主上下文 query 契约、壳 runtime 回包、URL hostCapabilities、H5 fallback、微信小程序 app.json.pagesprotocol.js 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、WebView source query、微信请求头来源标记、H5 路由保留字段、生产 / 开发 H5 与 API HTTPS 域名格式、三端桥接层结构、两端壳实现、Expo managed config、移动端 EAS build profile、移动端 production bundle 主站 URL、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 .js、Tauri Info.plist、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。H5 业务文件允许正常表单 placeholder 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但不得出现 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。

后续

  • 为 AI H5 sandbox 单独定义 GameBridge,禁止直接依赖 HostBridge。
  • 将宿主能力、支付渠道和分享策略补充进移动端发布检查清单。
  • 原生登录、渠道支付、远程推送、自动更新、崩溃上报和 analytics 等能力必须等真实 SDK、后端契约、发布流程和隐私口径确定后逐项接入。