---
name: event-kit-h5-integration
description: 当用户请求"开发 H5 活动页"、"H5 活动页接入 event_kit"、"H5 集成 event-kit"、"App 内打开 H5 页面"或需要在 H5 中调用 YAGO App 能力（跳转、进房、充值、分享等 Action）时触发。本技能是自包含的接入规范：App 注入的 ticket/language/安全区参数、postMessage 通信协议、全部 Action 定义与完整页面模板均已内嵌，无需读取 YAGO 仓库内部参考文件即可独立完成 H5 侧集成。
---

# H5 活动页集成 @yagolive/event-kit

本技能是**自包含的协议规范**。完成 H5 活动页集成所需的全部知识（App 注入参数、通信协议、Action 定义、示例代码）均已内嵌在本文档中，**不需要读取 YAGO 仓库内部文件**。App 侧（WebView 容器）的既有行为在下文中按"协议事实"描述，直接遵守即可。

---

## 0. 架构总览

- H5 页面运行在 App 内嵌的 react-native-webview 中（下文简称"容器"）。
- **App → H5（参数注入）**：页面加载前，容器把登录态与设备信息写入 URL query，并经 react-native-webview 的 `injectedJavaScriptObject` 注入 JSON 对象（见 1.2），H5 首屏即可同步读取，无需握手。
- **H5 → App（发 Action）**：`window.ReactNativeWebView.postMessage(JSON)`。
- **App → H5（回包）**：容器 `postMessage(JSON)` 回传执行结果，按 `messageId` 匹配。
- **App → H5（主动推送）**：容器可主动推送事件，`messageId` 前缀为 `dispatch_`。

---

## 1. App 注入参数（H5 可直接读取）

### 1.1 URL query 参数（容器加载前追加）

| 参数 | 说明 |
|------|------|
| `ticket` | 会话票据；**空字符串表示未登录** |
| `language` | 语言 |
| `hh` | 导航栏总高度（px）= 导航栏本体（约 48px）+ 状态栏高度，**已包含状态栏**；顶部安全区留白直接使用该值，无需再加 `sh` |
| `sh` | 状态栏高度（px） |
| `bi` | 底部安全距离（px） |
| `wh` | 页面窗口高度（px），仅当打开时指定了 height 才存在 |
| 其他 | 打开页面时路由透传的自定义参数 |

### 1.2 JS 对象注入（injectedJavaScriptObject，内容加载前注入）

App 通过 react-native-webview 的 `injectedJavaScriptObject` 属性注入一个 JSON 对象。**注意：注入的键不会直接挂到 `window` 上**，需通过桥对象上的方法取 JSON 字符串再自行解析（Android / iOS 行为一致）：

- 桥对象：`window.ReactNativeWebView`；
- 读取方式：`window.ReactNativeWebView.injectedObjectJson()` 返回注入对象的 **JSON 字符串**，用 `JSON.parse` 解析后使用；
- 注入时机：iOS 为 `WKUserScriptInjectionTimeAtDocumentStart`（文档开始时），Android 在桥就绪时注入，首屏即可同步读取；
- 浏览器调试环境没有该桥对象，读取封装需容错（解析失败回退 `{}` 并改用 URL query 兜底）。

读取封装（推荐直接复用）：

```javascript
function getInjectedParams() {
  var injected = {};
  try {
    var bridgeObj = window.ReactNativeWebView;
    var raw = bridgeObj && typeof bridgeObj.injectedObjectJson === 'function'
      ? bridgeObj.injectedObjectJson()
      : null;
    injected = raw ? JSON.parse(raw) : {};
  } catch (e) {
    injected = {};
  }
  return injected;
}
```

注入对象包含的键：

| 键 | 说明 |
|------|------|
| `ticket` | 会话票据，与 query 的 `ticket` 同值 |
| `language` | 语言 |
| `headerHeight` / `statusBarHeight` | 导航栏总高度（含状态栏，同 `hh`）/ 状态栏高度（`sh`） |
| `windowHeight` / `bottomInset` | 窗口高度 / 底部安全距离 |
| `constants` | 请求头合并对象（见下） |

`constants` 是 App 请求头合并后**剔除设备指纹 `x_k`** 的对象，可用字段：`BUNDLE_ID`、`OS`、`VERSION`、`LANGUAGE`、`TICKET`、`UNIQUE_ID`、`DISTINCT_ID`、`FCM_TOKEN`、`DEEP_LINK`、`scene`、`from_scene`、`source`、`source_id`。

另：`window.ReactNativeOS`（`'android'` 或 `'ios'`）由 `injectedJavaScriptBeforeContentLoaded` 脚本直接写在 `window` 上，可直接读取；浏览器中不存在。

### 1.3 ticket 使用规则（必须遵守）

1. **双通道注入**：`ticket` 同时出现在 URL query 与 injectedJavaScriptObject 注入对象（见 1.2）中，值一致。H5 侧：需要把 ticket 交给 H5 后端鉴权时读 **URL query**（H5 服务端可见）；纯前端 JS 使用读 **注入对象解析结果中的 `ticket`**（避免 URL 泄漏到日志/分享链接）。
2. **未登录态**：`ticket` 为空字符串。H5 必须处理空 ticket（提示登录或走游客流程），不要假设一定非空。
3. **敏感凭证**：不要持久化到 localStorage/cookie，不要拼进分享链接，不要在埋点中上报明文。
4. **`x_k` 不下发**：H5 拿不到设备指纹，也无需处理。
5. **注入时机**：所有参数在首屏内容加载前完成注入，顶层同步调用 `injectedObjectJson()` 并解析是安全的。
6. H5 请求自己后端需带 ticket 时，按与 App 一致的约定放 header（如 `TICKET`），不要在 query 中重复透传。

### 1.4 保留参数（H5 自定义参数请避开）

`theme` / `title` / `brandColor` / `tintColor` / `height` 会被 App 消费用于渲染原生头部（不会传给 H5）；`ticket/language/hh/sh/bi/wh` 为注入参数会被覆盖。另注意：链接中的 `inner_http` 前缀会被还原为 `http`（iOS universal link 兼容），部署时无需关心。

---

## 2. SDK 引入与初始化

npm 包名：`@yagolive/event-kit`。核心概念：`ActionCreator`（纯逻辑，创建 Action）、`ActionBridge`（通信）、Adapter（环境适配）。

### 2.1 CDN UMD（推荐纯 H5 页面）

```html
<!-- 完整包：ActionCreator + ActionBridge + 适配器；全局变量 window.__action_bridge__ -->
<script src="https://unpkg.com/@yagolive/event-kit/dist/js/action_bridge.umd.js"></script>
<!-- 或 jsdelivr：https://cdn.jsdelivr.net/npm/@yagolive/event-kit/dist/js/action_bridge.umd.js -->

<script>
  var { ActionBridge } = window.__action_bridge__;
  var bridge = ActionBridge.createDefault({ timeout: 15000 }); // 自动检测 RN WebView 环境
</script>
```

仅需创建 Action、不需要通信时用核心包 `action_creator.umd.js`（全局变量 `window.__action_creator__`，内含 `ActionCreator`）。

### 2.2 npm（ESM / CommonJS）

```javascript
import { ActionBridge, RNWebViewAdapter } from '@yagolive/event-kit';
const bridge = new ActionBridge(new RNWebViewAdapter({ timeout: 15000 }));
// 等价快捷方式：ActionBridge.createDefault({ timeout: 15000 })
```

React：`ActionBridgeProvider` + `useActionBridge`（`@yagolive/event-kit/react`）；Vue 3：`createActionBridgePlugin` + `useActionBridge`（`@yagolive/event-kit/vue`）。

### 2.3 环境判定与降级

- `window.ReactNativeWebView` 存在 → 真实 App 环境，用 `createDefault()`；
- 不存在（浏览器调试）→ 用 `createWithMock()` 走 MockAdapter，避免报错。

```javascript
var EK = window.__action_bridge__;
var bridge = window.ReactNativeWebView
  ? EK.ActionBridge.createDefault({ timeout: 15000 })
  : EK.ActionBridge.createWithMock(); // 浏览器调试降级
```

MockAdapter 支持：`mockResult(actionType, result)`、`mockError(actionType, error)`、`getCalls()`、`clearCalls()`、`clearMocks()`。

---

## 3. 通信协议

### 3.1 H5 → App（发送 Action）

```json
{ "type": "navigate", "data": { "screen": "profile", "params": { "userId": 123 } }, "messageId": "msg_1716364800000_ab12cd" }
```

- `messageId` 由 SDK 生成；**App 回包必须原样携带**，否则 Promise 挂起直到超时。
- 默认超时 30s；`bridge.sendAction(action, { timeout: 5000 })` 单次覆盖，`timeout <= 0` 禁用超时。
- 不期待回包用 `bridge.sendActionNoReply(action)`。
- 页面卸载时调用 `bridge.destroy()` 清理。

### 3.2 App → H5（回包）

```json
{ "messageId": "msg_...", "success": true, "result": { }, "error": "" }
```

`success=false` 时 `error` 为错误信息（如 `No handler for action type: xxx`），SDK reject Promise。**所有 Action 调用都必须 `.catch` 处理失败与超时。**

### 3.3 主动推送（App → H5）

容器主动推送时 `messageId` 以 `dispatch_` 开头，与请求回包同走 `message` 事件。H5 监听方式：

```javascript
window.addEventListener('message', function (event) {
  try {
    var data = JSON.parse(event.data);
    if (data && data.messageId && String(data.messageId).indexOf('dispatch_') === 0) {
      console.log('收到 App 推送', data);
    }
  } catch (e) {}
});
```

### 3.4 平台差异（SDK 已兼容，H5 无需处理）

Android 回包走 `document` 的 `message` 事件，iOS 走 `window`。**不要自行覆盖 `message`/`document` 监听去解析回包**，统一走 SDK；需要接收推送时按 3.3 叠加监听即可。

---

## 4. Action 定义（完整内嵌）

> **以官方最新文档为准**：本节速查基于本技能编写时点的 SDK 版本；Action 的新增与字段调整，请以 npm 上 @yagolive/event-kit 最新版参考文档为准：https://www.npmjs.com/package/@yagolive/event-kit

Action 序列化格式：`{"type":"<type>","data":{...}}`。所有 data 类均支持 `text` 字段（显示文案，可省略）。

### 4.1 type 枚举

`jump`、`navigate`、`replace`、`goback`、`enter_room`、`live_detect`、`onelink`、`ask`、`agency_invite`、`recharge`、`refresh_user_info`、`track`、`grant_benefits`、`verify_trade_password`、`upload_image`、`close`（容器扩展）、`config`（容器扩展）。

### 4.2 data 字段速查

| type | 便捷方法 | data 关键字段 | 效果 |
|------|----------|---------------|------|
| `jump` | `bridge.jump(path)` | `path` 必填（App 深链，如 `/web/<encoded-url>`、内部路由） | 深链跳转 |
| `navigate` | `bridge.navigate(screen, params?)` | `screen` 必填（App 屏幕名）、`params` | 导航到指定屏幕 |
| `replace` | `bridge.replace(screen, params?)` | 同 navigate | 替换当前屏幕 |
| `goback` | `bridge.goback()` | - | 返回上一页 |
| `enter_room` | `bridge.enterRoom(id)` | `id`、`match`（recommend/follow/agency）、`useReplace`、`callbacks` | 进入房间 |
| `live_detect` | `bridge.liveDetect()` | - | 活体检测，回传 token 等结果 |
| `onelink` | `bridge.onelink(url)` | `url` 必填、`share`（是否弹分享） | 生成/打开分享链接 |
| `ask` | `bridge.ask(id, {title, message})` | `id` 必填、`title`、`message`、`accepted` | 通用询问（title/message 非空弹确认框，均为空走静默模式） |
| `agency_invite` | `bridge.agencyInvite(id, type)` | `id` 必填、`type` 必填（to_be_member / to_be_subagency）、`agencyInfo`、`title`、`message` | 公会邀请 |
| `recharge` | `bridge.recharge(options?)` | `sku`（外部产品 ID）、`text` | 充值：携带 sku 时（Android）直接发起内购并回传订单，否则打开充值弹窗；iOS 携带 sku 会报错 |
| `refresh_user_info` | `bridge.refreshUserInfo()` | - | 刷新用户信息（触发宿主重拉 userInfo，如领奖后金币入账） |
| `track` | `bridge.track(name, params?, options?)` | `name` 必填（事件名）、`params`、`options` | 埋点上报 |
| `grant_benefits` | `bridge.grantBenefits(benefits, options?)` | `benefits` 必填（数组）、`title`、`message`、`okAction`、`closeable`、`duration`、`theme` | 权益下发弹窗 |
| `verify_trade_password` | `bridge.verifyTradePassword()` | - | 验证交易密码 |
| `upload_image` | `bridge.uploadImage(options?)` | `fileType`（user_avatar/room_image/other）、`sources`（picker/camera/both）、`cropping`（{width,height}）、`quality`（0-1） | 选图/拍照并上传，回传结果 |
| `close` | `bridge.close()` | - | 关闭当前 WebView 窗口 |
| `config` | `bridge.config(options?)` | `theme`（dark/light）、`brandColor`、`tintColor`、`title` | 配置原生头部主题/标题 |

补充字段细节：

- `enter_room` 的 `callbacks`：可包含 `open_gift_popup` / `open_game` / `open_activity` / `switch_room_type`，各带 `id` 等字段（进房后由 App 回调 H5 事件）。
- `agency_invite` 的 `agencyInfo`：`{ id, name, ownerId, ownerName, ownerAvatar }`。
- `grant_benefits` 的 `okAction`：接受一个完整 Action 对象（如 `bridge.createJumpActionWithData(...)`）。

### 4.3 Builder 链式配置（复杂 Action）

```javascript
var enterRoom = bridge.buildEnterRoom()
  .setId(1001)
  .setUseReplace(true)
  .setText('进入房间')
  .addOpenGiftPopupCallback(
    new bridge.ActionData.EnterRoom.OpenGiftPopupCallback().setId(1)
  );
var action = bridge.createEnterRoomActionWithData(enterRoom);
bridge.sendAction(action).then(function (r) { console.log(r); }).catch(function (e) { console.error(e); });
```

可用引用：`bridge.ActionCreator`（含 `ActionData` 全部数据类与 `ActionType` 枚举）、`bridge.buildXxx()` / `bridge.createXxxActionWithData(data)`。`action.toJson()` 可预览序列化结果（调试用）。

### 4.4 App 侧消费语义（H5 侧预期）

- `close`、`config` 由 WebView 容器直接处理；其余由 App 全局动作管理器分发执行。
- 每个 type 对应一个执行器；执行完成回 `{success:true, result}`，异常回 `{success:false, error}`；未注册的 type 回 `No handler for action type: xxx`。
- 因此 H5 调用后**必须等待回包**（或设置合理超时），并做好失败降级。

---

## 5. 标准接入流程（7 步，全部可独立完成）

1. **引入 SDK**（见 2.1/2.2），优先 CDN UMD，按环境降级 Mock（见 2.3）。
2. **读取注入参数**：用 `getInjectedParams()`（见 1.2）解析注入对象，解析失败或字段缺失时用 URL query 兜底；顶层同步读取即可。
3. **安全区适配**：顶部按 `hh` 留白（`hh` 已包含状态栏高度，避免被原生透明导航栏遮挡），底部按 `bi` 留白；viewport 加 `viewport-fit=cover`。
4. **ticket 处理**：空 ticket 走未登录分支（提示登录或游客流程）。
5. **业务调用**：用便捷方法（返回 Promise），**必须 catch**；需要配置时先 `bridge.config()`。
6. **本地联调**：浏览器直开走 Mock 模式验证页面逻辑与 Action JSON；用 `action.toJson()` / `mock.getCalls()` 检查。
7. **真机联调**：通过深链 `/web/{encodeURIComponent(h5Url)}` 在 App 内打开，验证 ticket 注入、安全区、各 Action 回包与推送监听。

---

## 6. 完整 H5 页面模板（可直接使用）

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <title>YAGO H5 活动页</title>
  <script>
    // 顶部安全区：按 App 注入的 hh（导航栏总高度，已含状态栏）预留空间
    document.writeln('<style>body{margin-top:' + (new URLSearchParams(location.search).get('hh') || 0) + 'px}</style>');
  </script>
  <script src="https://unpkg.com/@yagolive/event-kit/dist/js/action_bridge.umd.js"></script>
</head>
<body>
  <h1 id="title">加载中...</h1>
  <button id="btnGoBack">返回</button>
  <button id="btnEnterRoom">进入房间</button>

  <script>
    // 1. 读取 App 注入参数：injectedJavaScriptObject（JSON.parse）优先，URL query 兜底
    var search = new URLSearchParams(location.search);
    var injected = {};
    try {
      var bridgeObj = window.ReactNativeWebView;
      var raw = bridgeObj && typeof bridgeObj.injectedObjectJson === 'function'
        ? bridgeObj.injectedObjectJson()
        : null;
      injected = raw ? JSON.parse(raw) : {};
    } catch (e) { injected = {}; }
    var params = {
      ticket: injected.ticket !== undefined ? injected.ticket : (search.get('ticket') || ''),
      language: injected.language || search.get('language'),
      headerHeight: parseInt(injected.headerHeight || search.get('hh') || '0', 10),
      statusBarHeight: parseInt(injected.statusBarHeight || search.get('sh') || '0', 10),
      bottomInset: parseInt(injected.bottomInset || search.get('bi') || '0', 10)
    };

    // 2. 初始化 bridge（浏览器调试自动降级 Mock）
    var EK = window.__action_bridge__ || window.__action_creator__;
    var bridge = null;
    if (EK && EK.ActionBridge) {
      bridge = window.ReactNativeWebView
        ? EK.ActionBridge.createDefault({ timeout: 15000 })
        : EK.ActionBridge.createWithMock();
    }

    // 3. ticket 校验（未登录时可按业务引导登录或走游客流程）
    if (!params.ticket) {
      console.warn('[h5] 未登录，ticket 为空');
    }

    // 4. 统一调用封装：必须处理失败
    function call(promise) {
      if (!bridge) { console.error('[h5] bridge 未初始化'); return; }
      promise.then(function (result) {
        console.log('[h5] 成功', result);
      }).catch(function (error) {
        console.error('[h5] 失败', error);
        // 业务降级提示
      });
    }

    document.getElementById('btnGoBack').onclick = function () {
      call(bridge.goback());
    };
    document.getElementById('btnEnterRoom').onclick = function () {
      call(bridge.enterRoom(1001));
    };

    // 5. 监听 App 主动推送（messageId 以 dispatch_ 开头）
    window.addEventListener('message', function (event) {
      try {
        var data = JSON.parse(event.data);
        if (data && data.messageId && String(data.messageId).indexOf('dispatch_') === 0) {
          console.log('[h5] App 推送', data);
        }
      } catch (e) {}
    });

    document.getElementById('title').textContent =
      'os=' + (window.ReactNativeOS || 'browser') + ' lang=' + (params.language || '-');
  </script>
</body>
</html>
```

---

## 7. 与 App 侧配合（打开方式）

- **通用入口**（外部分享/临时链接）：深链 `/web/{encodeURIComponent(h5Url)}`，App 会做 URL 标准化后加载，注入 1.1/1.2 的参数。
- **固定活动屏幕**（常驻活动位）：URL 由**服务端配置下发**，配置类型 `APP_WEB_SCREEN`，`name` = 屏幕名（如 `Activity`），`value` = H5 URL。H5 团队无需关心 App 代码，只需与服务端约定屏幕名。
- 若同时在 yago 单仓内开发 App 侧屏幕，可直接套用以下模板（不需要读其他文件）：

```tsx
import createWebScreen from '@/screens/(web)/createWebScreen';
import type { WebScreenParams } from '@/screens/(web)/useWebScreen';

const XxxScreen = createWebScreen<RootParamList, 'Xxx'>();
export default XxxScreen;

declare global {
  interface RootParamList {
    Xxx?: WebScreenParams<{ /* 自定义透传参数 */ }>;
  }
}
```

然后运行 `yarn generate:nav` 重新生成导航注册；路由 params 中的自定义字段会被追加到 H5 URL query。

---

## 8. 常见问题排查

| 现象 | 排查 |
|------|------|
| `ticket` 为空 | 未登录为正常表现（注入空串）；已登录仍空则检查登录态 |
| Promise 一直 pending 后超时 | App 未回包或 messageId 不匹配；确认 type 已被 App 支持；用 `timeout` 缩短定位 |
| 浏览器中报 `postMessage` 相关错误 | 浏览器无 `window.ReactNativeWebView`，切 Mock 模式调试 |
| 回包收不到（Android） | 不要覆盖 `document` 的 `message` 监听，SDK 已按 `window.ReactNativeOS` 处理平台差异 |
| 页面顶部被原生导航栏遮挡 | 使用注入的 `hh` 做顶部留白（`hh` 已含状态栏，见第 5 节步骤 3） |
| 自定义 URL 参数丢失 | 与保留参数重名（`theme/title/brandColor/tintColor/height/ticket/language/hh/sh/bi/wh`）会被 App 消费或覆盖 |
| H5 自己调接口需要鉴权 | 用注入参数中的 `ticket` 或 `constants.TICKET`（经 `injectedObjectJson()` 解析，见 1.2）走 H5 后端；不要在 H5 内自建登录态 |

---

## 9. 附录：仓库内参考文件（可选，非必需）

以下文件仅当需要**核对 App 侧实现细节**时可读；按本技能文档即可完成 H5 集成，无需读取：

- `packages/@yagolive/event_kit/README.md` — SDK 完整 API 文档
- `packages/@yagolive/event_kit/index.html` — 本地联调测试页
- `src/components/common/CommonWebView/index.tsx`、`.../useCommonWebViewInject.ts` — App 侧容器实现
- `src/screens/(web)/useWebScreen.ts`、`.../createWebScreen.tsx` — App 侧屏幕脚手架
- `src/components/app/ActionManager/` — App 侧各 Action 执行器
