# TSGame HarmonyOS 应用框架设计与开发指南

> **配套文档**：本设计严格落地《[TSGame_原生与H5接口契约总规范](./TSGame_原生与H5接口契约总规范.md)》（下称《契约规范》）。**契约规范定义"对外必须一致的行为"，本文定义"HarmonyOS 侧如何优雅、高效地实现它"。** 二者配合即可让现有 H5 **零改动**运行。
>
> **设计目标**：现代化（ArkTS / ArkUI 声明式 / Stage 模型）、高性能（Web 保活、并发解压下载、零主线程阻塞）、架构优雅（分层 + 依赖倒置 + 能力插件化）、专业成熟（清晰边界、可测试、可观测、可演进）。
>
> **目标平台**：HarmonyOS NEXT（API 12+，建议 API 17/23），ArkWeb（方舟 Web）组件。

---

## 目录

1. [设计原则](#1-设计原则)
2. [Android → HarmonyOS 能力映射总表](#2-android--harmonyos-能力映射总表)
3. [总体架构（分层 + 模块）](#3-总体架构分层--模块)
4. [工程结构（模块化拆分）](#4-工程结构模块化拆分)
5. [核心一：JsBridge 桥引擎设计](#5-核心一jsbridge-桥引擎设计)
6. [核心二：能力插件框架（CapabilityProvider）](#6-核心二能力插件框架capabilityprovider)
7. [容器设计：双容器模型](#7-容器设计双容器模型)
8. [启动编排与配置/资源子系统](#8-启动编排与配置资源子系统)
9. [性能架构](#9-性能架构)
10. [横切关注点](#10-横切关注点)
11. [契约可追溯性矩阵](#11-契约可追溯性矩阵)
12. [实施路线图（里程碑）](#12-实施路线图里程碑)
13. [验收标准](#13-验收标准)
- [附录 A：ArkTS / ArkUI 实现适配清单](#附录-aarkts--arkui-实现适配清单)
- [附录 B：生产加固与合规清单](#附录-b生产加固与合规清单)

---

## 1. 设计原则

| 原则 | 说明 | 在本框架的体现 |
|---|---|---|
| **契约优先（Contract-First）** | H5 看到的协议是不可变契约，原生实现围绕契约展开 | 桥协议、handler 名、数据结构以《契约规范》为单一事实来源（SSOT），用 TS 类型固化 |
| **依赖倒置（DIP）** | 上层依赖抽象，不依赖具体平台实现 | 桥引擎依赖 `ICapability`/`IPlatformService` 接口，能力模块按需注入 |
| **能力插件化（Plugin）** | 每个原生能力是一个自注册插件 | `CapabilityProvider` 注册表，新增能力 = 新增一个 Provider，零侵入桥核心 |
| **单一职责 + 分层** | 桥、能力、配置、资源、平台服务各司其职 | 见 §3 分层 |
| **异步非阻塞** | IO/CPU 重活不上 UI 线程 | TaskPool/Worker 跑下载、解压、加解密；UI 线程只做编排与渲染 |
| **可观测（Observability）** | 全链路可日志、可埋点、可诊断 | 统一 `Logger` + `BridgeTracer`，每条桥消息可追踪 |
| **可演进** | 旧契约保留，新能力可叠加 | 契约层与实现层解耦；补齐项（QQ/抖音回调等）以新增 Provider 落地 |

---

## 2. Android → HarmonyOS 能力映射总表

> 这是把《契约规范》的 Android 机制翻译到 HarmonyOS 的"罗塞塔石碑"，框架的所有实现都基于此映射。

| 契约机制（Android） | HarmonyOS ArkWeb 等价物 | 说明 |
|---|---|---|
| `WebView`（X5/BridgeWebView） | `Web({ src, controller })` + `webview.WebviewController` | 声明式组件，Controller 控制行为 |
| `shouldOverrideUrlLoading` 拦截 `yy://` | **`onLoadIntercept`**（返回 `true` 拦截） | 桥协议核心；`onLoadIntercept` 在 loadUrl 与 iframe 加载时均触发，正好匹配 H5 用 iframe.src 发起 `yy://` |
| `loadUrl("javascript:...")` / `evaluateJavascript` | **`controller.runJavaScript(script)`**（带 Promise 回调） | 原生→H5 下发 |
| `onPageFinished` 注入桥 JS | **`onPageEnd`** 中 `runJavaScript(桥JS)` | 注入 `WebViewJavascriptBridge.js` |
| `addJavascriptInterface(obj, name)`（@JavascriptInterface） | **`javaScriptProxy` 属性** 或 `controller.registerJavaScriptProxy()` | 仅通用网页容器 `settings` 用 |
| `onProgressChanged` | **`onProgressChange`** | 进度=100% 触发 `appservice`/`setPostUrl` |
| `onResume/onPause/onStop`（前后台） | `UIAbility.onForeground/onBackground` + 页面 `onPageShow/onPageHide` | 推送 `appservice` |
| WebSettings.setJavaScriptEnabled | `.javaScriptAccess(true)` | |
| setDomStorageEnabled | `.domStorageAccess(true)` | |
| setAllowFileAccess | `.fileAccess(true)`（默认开） | 访问本地文件系统 |
| setAllowUniversalAccessFromFileURLs（file:// 跨域） | **无直接等价属性**；用官方跨域方案：`onInterceptRequest`/`WebSchemeHandler` 自定义协议接管本地资源，或以自定义 scheme 加载首页 | 见 §7.3；这是 file:// 加载下 H5 发 XHR/fetch 取本地资源的关键，**M2 必须验证** |
| setMixedContentMode(ALWAYS_ALLOW) | `.mixedMode(MixedMode.All)` | |
| setCacheMode(LOAD_NO_CACHE) | `.cacheMode(CacheMode.None)` | 主容器禁缓存 |
| setGeolocationEnabled | `.geolocationAccess(true)` | |
| setSupportZoom(false) | `.zoomAccess(false)` | |
| setWebContentsDebuggingEnabled | `webview.WebviewController.setWebDebuggingAccess(true)` | 在 `aboutToAppear` 设置 |
| `startActivityForResult`/`setResult`（容器间回传） | 路由参数 + 回调 / `emitter` 事件 / `AppStorage` | 通用网页容器回传 `data`（结果码 101 语义）见 §7.2 |
| Intent extra | `router`/`Navigation` 参数对象 | |
| SharedPreferences | `@ohos.data.preferences`（KV） | `urlpath`/`upurlpath` 等 |
| 本机 HTTP server（截图上传） | **`@ohos.net.http` 是客户端、无内置服务端**；用 `@ohos.net.socket`(TCP) 自建轻量 HTTP 服务，或 NAPI 原生 server（如 cpp-httplib） | 监听本机端口，地址经 `setPostUrl` 告知 H5 |
| OkHttp | `@ohos.net.http` / `rcp`（Remote Communication Kit） | 远程配置、下载 |
| zip 解压 | `@ohos.zlib`（`decompressFile`） | TaskPool 内执行 |
| 友盟/Bugly | HUAWEI Analytics Kit / APM / Crash Service | 可观测，非契约 |
| 微信/QQ/抖音/高德 SDK | 各厂商 HarmonyOS SDK | 能力 Provider 内对接 |

---

## 3. 总体架构（分层 + 模块）

采用**自上而下五层 + 横切层**，依赖方向单向向下，跨层只依赖接口。

```
┌────────────────────────────────────────────────────────────────────────┐
│  ① 应用/编排层  App & Orchestration                                       │
│     EntryAbility · StartupOrchestrator · 路由(Navigation) · 全局DI容器     │
├────────────────────────────────────────────────────────────────────────┤
│  ② 容器层  Containers (ArkUI Pages)                                       │
│     BridgeGameContainer(大厅/子游戏)  │  GenericWebContainer(通用网页)     │
├────────────────────────────────────────────────────────────────────────┤
│  ③ 桥引擎层  Bridge Engine            ④ 能力层  Capabilities (插件)        │
│     BridgeController · MessageCodec   │  Share/Login/Pay/Location/Audio/  │
│     HandlerRegistry · QueueFlusher    │  Shake/Device/Clipboard/Net/...   │
│     SettingsProxy(通用容器)           │  每个 = 一个 CapabilityProvider    │
├────────────────────────────────────────────────────────────────────────┤
│  ⑤ 领域服务层  Domain Services                                            │
│     ConfigManager · ResourceManager · AppDataInjector · VersionResolver   │
├────────────────────────────────────────────────────────────────────────┤
│  ⑥ 平台服务层  Platform Services (对 SDK 的薄封装)                         │
│     HttpClient · Downloader · Unzipper · KvStore · PermissionGuard ·      │
│     LocalUploadServer · FileSystem · TaskScheduler(TaskPool/Worker)        │
└────────────────────────────────────────────────────────────────────────┘
  横切层  Cross-cutting： Logger/Tracer · ErrorCenter · EventBus · DIContainer · Contracts(TS类型)
```

**关键约束**：
- 桥引擎层（③）**不认识任何具体能力**，只持有 `HandlerRegistry`；能力层（④）启动时把自己的 handler 注册进去 → **桥核心对能力数量零感知**。
- 能力层（④）通过平台服务层（⑥）使用系统/厂商 SDK，**绝不**直接被容器层调用（保持单向）。
- `Contracts` 横切层用 TypeScript `interface`/`enum`/常量固化《契约规范》的 handler 名与数据结构，**全工程唯一来源**。

---

## 4. 工程结构（模块化拆分）

按 HarmonyOS **HAR/HSP 多模块**组织，强边界、可独立编译、可并行开发：

```
TSGameHarmony/
├── entry/                         # 入口 HAP（应用/编排层 ①②）
│   └── src/main/ets/
│       ├── entryability/EntryAbility.ets
│       ├── startup/StartupOrchestrator.ets
│       ├── pages/
│       │   ├── SplashPage.ets             # 启动引导（对应 weclomeactivity1）
│       │   ├── BridgeGameContainer.ets    # 大厅/子游戏容器
│       │   └── GenericWebContainer.ets    # 通用网页容器(对应 openwebActivity1)
│       └── di/AppModule.ets               # 组装根：把各 Provider 注入桥
│
├── feature_bridge/   (HAR)        # 桥引擎层 ③ —— 与业务解耦的可复用桥
│   └── ets/
│       ├── BridgeController.ets
│       ├── MessageCodec.ets · Message.ets
│       ├── HandlerRegistry.ets
│       ├── SettingsProxy.ets              # 通用容器 @javaScriptProxy 对象
│       └── assets/WebViewJavascriptBridge.js   # 直接复用 lzyzsd 原文件
│
├── feature_capabilities/ (HAR)    # 能力层 ④ —— 每个能力一个文件夹
│   └── ets/
│       ├── CapabilityProvider.ets         # 插件接口 + 注册表
│       ├── share/ login/ pay/ location/ audio/ shake/
│       ├── device/ clipboard/ network/ vibrate/ scan/ camera/ photo/ room/
│       └── index.ets                      # 汇总导出 provideAll()
│
├── domain_resource/  (HAR)        # 领域服务层 ⑤
│   └── ets/ ConfigManager · ResourceManager · AppDataInjector · VersionResolver
│
├── platform/         (HAR)        # 平台服务层 ⑥
│   └── ets/ HttpClient · Downloader · Unzipper · KvStore · PermissionGuard ·
│            LocalUploadServer · FileSystem · TaskScheduler
│
├── contracts/        (HAR)        # 横切：契约类型（SSOT）
│   └── ets/ Handlers.ets(枚举所有handler名) · dto/*.ets(数据结构) · Errors.ets
│
└── common/           (HAR)        # 横切：Logger/Tracer/EventBus/DI/Result<T>
```

> **为何这样切**：`feature_bridge` 与 `contracts` 不含任何业务，可被未来其他 H5 壳复用；`feature_capabilities` 可按渠道裁剪（如海外版去掉微信支付）；`domain_resource`/`platform` 可单测。

---

## 5. 核心一：JsBridge 桥引擎设计

> 这是"H5 零改动"的命门。目标：**100% 复刻 lzyzsd/JsBridge 协议**（《契约规范》§2），但用 ArkTS 优雅实现。

### 5.1 协议落地（与契约逐条对应）

| 协议要素 | 实现 |
|---|---|
| 注入 `WebViewJavascriptBridge.js` | 在 `Web().onPageEnd` 回调里 `controller.runJavaScript(bridgeJs)`；JS 文件**直接复用库原文件**，保证 `window.WebViewJavascriptBridge` API 与事件 `WebViewJavascriptBridgeReady` 完全一致 |
| H5→原生（`yy://`） | `Web().onLoadIntercept`（拦截范围含 **iframe 导航**，正好匹配 H5 用 `iframe.src='yy://…'` 发起调用）中：`URLDecode` 后，`yy://return/` 前缀→`handleReturnData`（回执/队列）；其余 `yy://` 前缀→`flushMessageQueue()`；返回 `true` 拦截，其余返回 `false` 放行 |
| 取队列 `_fetchQueue()` | ⚠️ **队列不靠 `runJavaScript` 返回值取回**。`flushMessageQueue` 先以函数名 `_fetchQueue` 预登记回调，再 `runJavaScript("WebViewJavascriptBridge._fetchQueue();")`（不读返回值）；H5 内 `_fetchQueue` 把 `iframe.src` 置为 `yy://return/_fetchQueue/<队列JSON>`，**再次经 `onLoadIntercept` → `handleReturnData`** 路由到该回调。与 Android `BridgeWebView` 实现完全一致 |
| 原生→H5 单条 | `controller.runJavaScript("WebViewJavascriptBridge._handleMessageFromNative('"+json+"');")` |
| Message 结构 | `{handlerName, data, callbackId, responseId, responseData}`（§2.4），`MessageCodec` 负责与 lzyzsd 完全一致的转义（注意原库对 `\` 与 `"` 的二次转义） |
| callbackId 生成 | `JAVA_CB_<自增>_<时间戳>`（格式可自定，H5 只原样回带） |
| 启动消息队列 | 页面 `onPageEnd` 前 native 若 callHandler，先入 `startupMessage` 队列，注入桥 JS 后补发（§2.6） |

> ⚠️ **M1 必须先验证的高风险点**：不同 ArkWeb 版本对**自定义 scheme（`yy://`）的 iframe 导航**是否稳定触发 `onLoadIntercept` 存在差异。M1 桥引擎自测时，**首先**用最小回显 H5 确认 `yy://` 能被 `onLoadIntercept` 捕获；若个别版本不触发，回退方案优先级：① `onInterceptRequest` + `WebSchemeHandler`（注册自定义 scheme，能力更强、可取 POST 体）；② 极端情况下改桥协议为 `javaScriptProxy` 注入一个 `_bridgeNative.postMessage(json)` 同步方法替代 `yy://` 通道（**此法需同步改写注入的 `WebViewJavascriptBridge.js` 的 `_doSend`/`_fetchQueue`，但对 H5 仍透明，`window.WebViewJavascriptBridge` 对外 API 不变**）。三种方案对 H5 均零改动。|

### 5.2 BridgeController（桥控制器，每个 Bridge 容器持有一个）

职责：管 WebviewController、URL 拦截分发、handler 注册、出入站消息编解码与回调表。**对外暴露 `registerHandler` / `callHandler`，与 Android 端 API 同名同义**。

```typescript
// feature_bridge —— 设计级伪代码（ArkTS 风格，省略 import）
export type BridgeHandler = (data: string, callback: (resp: string) => void) => void;

export class BridgeController {
  private controller: webview.WebviewController;
  private registry: HandlerRegistry;                       // 注入：能力层填充
  private responseCallbacks = new Map<string, (d: string)=>void>();
  private startupMessages: Message[] | null = [];          // 页面就绪前的积压
  private uniqueId = 0;

  // —— H5 → 原生：在 Web().onLoadIntercept 调用 ——
  onLoadIntercept(url: string): boolean {
    const u = decodeURIComponent(url);
    if (u.startsWith('yy://return/')) { this.handleReturnData(u); return true; }
    if (u.startsWith('yy://'))        { this.flushMessageQueue(); return true; }
    return false;                                          // 正常导航
  }

  // —— 原生 → H5：能力层/容器调用 ——
  callHandler(name: string, data: string, cb?: (resp: string)=>void): void {
    const m = new Message(); m.handlerName = name; m.data = data;
    if (cb) { const id = `JAVA_CB_${++this.uniqueId}_${SystemClock.now()}`;
              this.responseCallbacks.set(id, cb); m.callbackId = id; }
    this.queue(m);
  }

  registerHandler(name: string, h: BridgeHandler): void { this.registry.put(name, h); }

  // —— 页面就绪：在 Web().onPageEnd 调用 ——
  onPageEnd(): void {
    this.controller.runJavaScript(BRIDGE_JS);              // 注入桥
    if (this.startupMessages) { this.startupMessages.forEach(m => this.dispatch(m));
                                this.startupMessages = null; }
  }

  // —— ⚠️ 关键：触发 H5 交出待发队列。注意队列【不是】靠 runJavaScript 返回值取回，
  //         而是 _fetchQueue() 在 H5 内把 iframe.src 置为 'yy://return/_fetchQueue/<data>'，
  //         再次被 onLoadIntercept 捕获 → handleReturnData 路由到这里注册的 '_fetchQueue' 回调。
  private flushMessageQueue(): void {
    // 以函数名 '_fetchQueue' 作为 key 预登记回调（与 callbackId 共用同一张 responseCallbacks 表）
    this.responseCallbacks.set('_fetchQueue', (queueJson: string) => {
      const list = MessageCodec.toArray(queueJson);        // H5 待发消息数组
      for (const m of list) {
        if (m.responseId) {                                // 是 H5 对"原生 callHandler"的回执
          this.responseCallbacks.get(m.responseId)?.(m.responseData);
          this.responseCallbacks.delete(m.responseId);
        } else {                                           // 是 H5 主动 callHandler
          const respFn = m.callbackId
            ? (d: string) => this.queue(Message.response(m.callbackId!, d))
            : (_: string) => {};
          const handler = this.registry.get(m.handlerName) ?? this.registry.default();
          handler(m.data, respFn);                         // ← 分发到能力层
        }
      }
    });
    // 仅触发，不读取返回值
    this.controller.runJavaScript('WebViewJavascriptBridge._fetchQueue();');
  }

  // —— H5 → 原生 的回执通道：yy://return/<functionName>/<data> ——
  // 既处理 _fetchQueue（队列回传），也处理普通 callbackId 回执
  private handleReturnData(url: string): void {
    const fn = parseFunctionFromReturnUrl(url);            // 如 '_fetchQueue' 或 'JAVA_CB_x_y'
    const data = parseDataFromReturnUrl(url);
    const cb = this.responseCallbacks.get(fn);
    if (cb) { cb(data); this.responseCallbacks.delete(fn); }
  }

  private queue(m: Message): void {
    if (this.startupMessages) this.startupMessages.push(m); else this.dispatch(m);
  }
  private dispatch(m: Message): void {
    const json = MessageCodec.escape(m.toJson());
    this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
  }
}
```

> 设计要点：
> - `BridgeController` **完全不 import 任何能力**；能力通过 `registry` 注入。桥引擎成为独立 HAR，可复用、可单测。
> - **务必忠实复刻"队列经 `yy://return/_fetchQueue/` 回传"的机制**，不要图省事改读 `runJavaScript` 的返回值——除非你同时改写注入的 `WebViewJavascriptBridge.js`（不推荐，破坏与成熟协议的一致性，易在边界场景出错）。`responseCallbacks` 一张表同时承载"函数名 key（`_fetchQueue`）"与"callbackId key（`JAVA_CB_*`）"两类回执，与 Android 端实现完全一致。
> - 上文 `this.controller: webview.WebviewController` 仅为示意。**为可测试性，桥应依赖抽象 `IWebController`（见 §5.6），由容器注入真实 `WebviewController` 适配器**，单测时注入 mock。
> - **必须设置 `DefaultHandler`（空实现）**：`flushMessageQueue` 分发时，`handler = registry.get(name) ?? registry.default()`。任何**未注册**的 handler 名都落到默认空实现而**不抛错**（对齐 Android `BridgeWebView.defaultHandler`）。这是"H5 调用永不报错"的最后防线，也是暂缓能力（§6.5）的安全网底座。

### 5.5 线程模型（P0，必须遵守）⚠️

> Android 原版 `BridgeWebView.dispatchMessage` 明确要求**只有在主线程才下发消息**（源码 `Thread.currentThread()==Looper.getMainLooper()` 才 `loadUrl`）。HarmonyOS 同理：**`runJavaScript` 只能在 UI（主）线程调用**。而能力回调（定位 `locationChange`、支付 SDK 回调、传感器、各类系统广播、TaskPool 完成）**经常发生在非 UI 线程**，若直接 `callHandler` → `runJavaScript` 会抛异常或行为异常。

**强制规则**：
1. **所有出站下发（`dispatch`/`runJavaScript`）必须在 UI 线程执行**。`BridgeController` 内部对 `dispatch` 做线程守卫：非 UI 线程则投递到 UI 线程任务队列。
2. **入站分发（`flushMessageQueue` 触发的 handler 回调）默认就在 UI 线程**（`onLoadIntercept` 在 UI 线程回调），handler 内若开了子线程做重活，回主线程再 `callback`/`callHandler`。
3. 能力 Provider 的异步事件回传统一经 `bridge.callHandler`，由桥内部保证线程切换，**Provider 不需自己关心线程**。

```typescript
// BridgeController 内：UI 线程守卫（示意）
private uiContext: UIContext;                 // 由容器在 aboutToAppear/onPageShow 注入
private dispatch(m: Message): void {
  const run = () => {
    const json = MessageCodec.escape(m.toJson());
    this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
  };
  if (isOnUiThread()) run();
  else this.uiContext.runScopedTask(run);     // 或 emitter/AppStorage 投递回 UI 线程
}
```

> 实现方式可选：① UIContext 的 UI 线程任务；② `@ohos.events.emitter` 在 UI 线程订阅、能力线程 emit；③ `AppStorage`/`LocalStorage` 状态驱动。无论哪种，**对外保证：能力随便在哪个线程 `callHandler`，最终都在 UI 线程 `runJavaScript`**。

### 5.6 IWebController 抽象（P2，支撑单测）

桥不直接绑死系统类，依赖最小接口，便于 mock 与未来替换内核：

```typescript
export interface IWebController {            // 桥只用到这几个能力
  runJavaScript(script: string): void;
  runJavaScript(script: string, cb: (err: Error|null, ret: string)=>void): void;
  loadUrl(url: string): void;
}
// 生产：WebviewControllerAdapter implements IWebController（包一层 webview.WebviewController）
// 测试：FakeWebController implements IWebController（记录脚本、模拟 yy:// 回执）
```
> 这样 `feature_bridge` HAR 不 import `@kit.ArkWeb` 的具体类，`BridgeController`/`MessageCodec`/`VersionResolver` 均可纯逻辑单测，§13 的 ≥80% 覆盖率目标方可达成。

### 5.3 同步返回 vs 异步推送

《契约规范》区分两类返回，框架用同一套 `callback` 表达：
- **同步返回**（如 `getTime`/`getnetwork`/`getlocationinfo` 主动取）：handler 内立即 `callback(value)`。
- **异步推送**（如定位结果、支付回调）：能力层在事件到达时调用 `bridge.callHandler('出站名', data)`。

### 5.4 数据载荷的"裸字符串 vs JSON"陷阱

`MessageCodec` 不擅自 JSON 化 data。能力层按《契约规范》§8/§9 **逐 handler** 决定：`getTime` 回裸串、`getwifiLevel` 回 JSON 串。`contracts` 层为每个 handler 声明 `payloadType: 'raw' | 'json'` 与 DTO，编译期约束，杜绝写错。

---

## 6. 核心二：能力插件框架（CapabilityProvider）

> 让"新增/裁剪一个原生能力"成为零侵入操作，是框架成熟度的体现。

### 6.1 插件接口

```typescript
export interface CapabilityProvider {
  readonly name: string;                                    // 如 'location'
  // 把本能力的入站 handler 注册到桥；并持有 bridge 以便异步推送
  register(bridge: BridgeController, ctx: CapabilityContext): void;
  onForeground?(): void;  onBackground?(): void;            // 生命周期联动
  onDestroy?(): void;
}
export interface CapabilityContext {
  uiAbilityContext: common.UIAbilityContext;
  config: ConfigManager; platform: PlatformServices; log: Logger;
}
```

### 6.2 注册表 + 组装根（DI）

```typescript
// entry/di/AppModule.ets —— 组装根：唯一知道"全部能力"的地方
export function buildCapabilities(): CapabilityProvider[] {
  return [
    new ShareProvider(), new LoginProvider(), new PayProvider(),
    new LocationProvider(), new AudioProvider(), new ShakeProvider(),
    new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(),
    new VibrateProvider(), new ScanProvider(), new CameraProvider(),
    new PhotoProvider(), new NavProvider(),                          // OpenurlTitleData/SwitchOverGameData/backgameData
    new AppSystemProvider(),                                          // orientation/browser/finsh/openApplyDownloadpath/notification
    // —— 本期暂不集成：用占位桩注册，保证 H5 调用不报错（见 §6.5），未来换回真实 Provider ——
    new RoomStubProvider(),   // 视频房（声网）暂缓  → 真实版 RoomProvider
    new PayStubProvider(),    // 支付暂缓            → 真实版 PayProvider
    // 闲聊：无 H5 桥接口，无需注册（默认 handler 兜底）
  ];
}
// 容器初始化时：
capabilities.forEach(p => p.register(bridge, ctx));
```

#### 44 个入站 handler → Provider 归属（确保零孤儿、可逐条核对《契约规范》§8）

| Provider | 负责的入站 handler |
|---|---|
| `ShareProvider` | `friendsSharetypeUrlToptitleDescript` |
| `LoginProvider` | `accreditlogin` |
| `PayProvider` （本期=`PayStubProvider` 桩，§6.5） | `paybrowser`、`getGameplay`(旧版可选) |
| `LocationProvider` | `startlocation`、`getlocationinfo` |
| `AudioProvider` | `prepareaudio`、`mediaTypeAudio`、`srcIsloop`、`voicePlaying` |
| `ShakeProvider` | `startshake`、`SwitchShake`、`stopshake` |
| `DeviceProvider` | `getTime`、`getphonestate`、`getbattery`、`getwifiLevel`、`getcompareCode`、`getphoneInfo`、`getmarketname`、`getothername`、`getOther` |
| `ClipboardProvider` | `gameCopytext`、`gamepastetext` |
| `NetworkProvider` | `getnetwork` |
| `VibrateProvider` | `vibrator`、`repeatvibrator`、`canclevibrator` |
| `ScanProvider` | `opensaoma` |
| `CameraProvider` | `opencamera` |
| `PhotoProvider` | `getphoto` |
| `RoomProvider` （本期=`RoomStubProvider` 桩，§6.5） | `createRoom`、`exitRoom`、`getVideoinfo`、`DragViewvideoIsshow` |
| `NavProvider` | `OpenurlTitleData`、`SwitchOverGameData`、`getGameinstall`、`backgameData`(New) |
| `AppSystemProvider` | `orientation`、`browser`、`finsh`、`openApplyDownloadpath`、`notification`(空实现) |

> 合计 **44 个入站 handler 全部有且仅有一个归属**。出站 handler（§9）由对应 Provider 在事件到达时 `bridge.callHandler` 推送（如 `LocationProvider→getlocationinfo`、`PayProvider→PayuserPaytypePaystate`、`AudioProvider→getaudiourl/gameui_*`、`DeviceProvider→getBattery/getwifiLevel/phonestate`、`ScanProvider→getsaomaData`、`CameraProvider→getcameraaAddress`、`PhotoProvider→getphoto`、`ShareProvider→sharesuccess`、`LoginProvider→sharelogin`、`ShakeProvider→shakeEnd`、`NavProvider→getWebdata`、容器→`appservice/setPostUrl`）。

### 6.3 一个 Provider 的样板（以定位为例）

```typescript
export class LocationProvider implements CapabilityProvider {
  readonly name = 'location';
  private bridge!: BridgeController; private ctx!: CapabilityContext;

  register(bridge: BridgeController, ctx: CapabilityContext): void {
    this.bridge = bridge; this.ctx = ctx;
    bridge.registerHandler(H.startlocation, (data, _cb) => this.start(data === '1'));
    bridge.registerHandler(H.getlocationinfo, (_d, cb) => cb(this.lastOrError()));
  }
  private start(continuous: boolean): void {
    geoLocationManager.on('locationChange', loc => {
      this.bridge.callHandler(H.getlocationinfo, JSON.stringify(toMapLocationInfo(loc)));
      if (!continuous) geoLocationManager.off('locationChange');
    });
  }
  private lastOrError(): string { /* 返回 MaplocationInfo JSON 或 {errorCode:-1,...} */ }
}
```

> 同理：`ShareProvider`/`LoginProvider`/`PayProvider` 对接微信/QQ/抖音/支付 HarmonyOS SDK；`AudioProvider` 用 `AVPlayer`/`SoundPool`；`ScanProvider` 用 Scan Kit；`CameraProvider` 用 Camera Picker；`DeviceProvider` 用 deviceInfo/telephony/`@ohos.batteryInfo`/`@ohos.wifiManager`。**所有 handler 名、参数、回传结构严格照《契约规范》§8/§9/§12。**
>
> **`ShareProvider` 的"截图分享"内部流程**（契约 §10.1 的 `type=="2"`）：`friendsSharetypeUrlToptitleDescript` 收到 `type=="2"` 时，由 `ShareProvider` 内部 `runJavaScript("...canvas.toDataURL...")` 取当前页 Canvas 的 base64（失败可回退原生截图），再经平台层 `LocalUploadServer`（其地址此前已由 `setPostUrl` 告知 H5）上传/或直接交分享 SDK。此为**内部实现**，不新增对外 handler，对 H5 透明。

### 6.4 补齐项以"新增 Provider/增强"落地（《契约规范》§14.2）

| 待补齐 | 设计方案 |
|---|---|
| QQ/抖音分享结果回传 | `ShareProvider` 统一在分享 SDK 回调里 `callHandler('sharesuccess', {success,type})`，补齐三端一致 |
| 抖音/快手登录 | 新增 `DouyinLoginProvider`，对接开放平台，**回传 code**（而非用户资料），由服务端换 token |
| 主动截图 | 新增入站 handler `getCanvasBase64`（向后兼容、H5 可选用），`runJavaScript` 调 `canvas.toDataURL` 取回 base64 |
| AppSecret/API_KEY 明文 | **下沉服务端**：登录走 code 模式，支付签名由服务端出，客户端不存密钥 |

### 6.5 暂不实现的能力：占位桩（No-op Stub），保证 H5 调用不报错/不卡死

> **范围**：本期**视频房**、**支付**、**闲聊**先不集成。但**接口必须"留空"**——即仍把对应 handler 注册进桥，只是实现为空操作。否则 H5 调用时行为不确定，且若 H5 传了 `responseCallback` 还可能**永久挂起**。

**两道安全网（缺一不可）**：
1. **桥默认 handler 兜底**：`BridgeController` 必须设置一个 `DefaultHandler`（空实现），任何**未注册**的 handler 名都路由到它，不抛错（对齐 Android `BridgeWebView.defaultHandler`）。这是最后防线。
2. **显式占位 Provider**：对"明确暂缓"的能力，提供 `StubProvider` 显式注册占位 handler——行为确定、可日志、可灰度开启，优于依赖默认兜底。

**占位桩的实现规则**：
- **void 型 handler**（H5 不期望同步返回）：空实现（可选 `log`/轻提示"功能暂未开放"），**不要**调用任何 `callHandler` 出站。
- **有同步返回的 handler**（H5 传了 `responseCallback`）：**必须回一个安全默认值**，否则 H5 卡死。如返回 `'0'`、空串 `''` 或合法空 JSON。
- **绝不**触发该能力的出站 handler（如不 `callHandler('getVideoinfo')`、不 `callHandler('PayuserPaytypePaystate')`），H5 收不到推送即按"无事发生"处理。

**本期占位清单**：

| 能力 | 占位 Provider | 入站 handler（注册为桩） | 桩行为 | 关联出站（**不触发**） |
|---|---|---|---|---|
| 视频房（声网） | `RoomStubProvider` | `createRoom`、`exitRoom`、`getVideoinfo`、`DragViewvideoIsshow` | 全部 void，空实现 + 日志 | `getVideoinfo`(uid 推送) |
| 支付 | `PayStubProvider` | `paybrowser`、`getGameplay`(旧) | void，空实现（可轻提示"支付暂未开放"） | `PayuserPaytypePaystate` |
| 闲聊 | —— | **无 H5 接口**（《契约规范》§10.4：`sgapi` 是闲聊且全注释、无 `registerHandler`） | 无需桩；H5 若误调，走默认 handler 兜底 | —— |

```typescript
// 占位 Provider 样板：注册即留空，H5 调用安全无副作用
export class RoomStubProvider implements CapabilityProvider {
  readonly name = 'room(stub)';
  register(bridge: BridgeController, ctx: CapabilityContext): void {
    const noop: BridgeHandler = (_d, _cb) => { ctx.log.i('room not integrated yet'); };
    bridge.registerHandler(H.createRoom, noop);
    bridge.registerHandler(H.exitRoom, noop);
    bridge.registerHandler(H.getVideoinfo, noop);
    bridge.registerHandler(H.DragViewvideoIsshow, noop);
  }
}
export class PayStubProvider implements CapabilityProvider {
  readonly name = 'pay(stub)';
  register(bridge: BridgeController, ctx: CapabilityContext): void {
    bridge.registerHandler(H.paybrowser, (_d, _cb) => ctx.log.i('pay not integrated yet')); // 可 Toast 提示
  }
}
```

> **组装根切换**：`buildCapabilities()` 里把 `new RoomProvider()`/`new PayProvider()` 暂时替换为 `new RoomStubProvider()`/`new PayStubProvider()`。**契约 handler 名/数量不变**，未来集成时换回真实 Provider 即可，**对 H5 始终零改动、零报错**。
> **闲聊**：本就没有 H5 桥接口，无需任何桩；默认 handler 兜底足以保证任何误调用不报错。

---

## 7. 容器设计：双容器模型

《契约规范》明确有**两套**互不相同的 H5↔原生机制，框架对应**两类容器组件**。

### 7.1 BridgeGameContainer（大厅 + 子游戏）

对应 `webviewActivity`/`NewwebviewActivity`。基于桥协议。

```typescript
@Component
export struct BridgeGameContainer {
  private controller = new webview.WebviewController();
  private bridge = new BridgeController(this.controller);

  // ⚠️ 仅 debug 包开启远程调试；release 包必须关闭（安全加固，见附录 B）
  aboutToAppear(): void { if (BuildProfile.DEBUG) webview.WebviewController.setWebDebuggingAccess(true); }

  build() {
    Web({ src: this.entryUrl(), controller: this.controller })
      .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
      .mixedMode(MixedMode.All).cacheMode(CacheMode.None)
      .geolocationAccess(true).zoomAccess(false)
      .onControllerAttached(() => { /* 注入对象(本容器不需要)、设 UA(默认即可) */
                                    buildCapabilities().forEach(p => p.register(this.bridge, ctx)); })
      .onLoadIntercept((e) => this.bridge.onLoadIntercept(e.data.getRequestUrl()))
      .onPageEnd(() => { this.bridge.onPageEnd(); })        // 仅注入桥 + 补发启动队列
      .onProgressChange((e) => { if (e.newProgress === 100 && !this.firstDone) {
          this.firstDone = true;                            // 首次到 100% 才推
          this.bridge.callHandler('appservice', '1');       // 前台
          this.bridge.callHandler('setPostUrl', uploadUrl()); } })
  }
  // 子游戏切换 SwitchOverGameData → 仅 controller.loadUrl(新目录 index.html?Launchtype=1)
}
```

> ⚠️ **时序铁律**：`app_data.js` 必须在本容器**加载页面之前**就由 `AppDataInjector` 写入大厅目录（见 §8.4，发生在 `StartupOrchestrator` 的 `INJECT_APP_DATA` 步、早于进入本容器）——因为 H5 首页用 `<script src="app_data.js">` 在**加载时同步读取**。**切勿**放到 `onPageEnd`/`onProgressChange` 里推，那时页面已加载完、H5 早已读过 `app_data.js`，为时已晚。`setPostUrl`/`appservice` 则按契约在首次进度 100% 时下发（与 Android `onProgressChanged==100` 一致）。

- **入口 URL**：`weburl` 空 → `file://<解压根>/gamehall/index.html?Launchtype=0`；非空 → `http://<weburl 去-换/>?Launchtype=0`（《契约规范》§3/§5）。
- **前后台**：`UIAbility.onForeground/onBackground` → `callHandler('appservice','1'|'2')`。
- **子游戏切换**（路径 A，`SwitchOverGameData`）：M2/M4 先用同容器 `controller.loadUrl()` 到目标目录跑通；**M5 演进为双 Web 保活模型（见 §9）**。

> **🔴 大厅/子游戏激活模型（M5 确认，2026-06-25）**：
> - **双 Web 槽**：**大厅 Web 常驻**；**子游戏 Web 进入时创建、返回大厅时销毁**（子游戏不保活）。
> - **同时只有一个激活**（激活=联网+收发包+界面更新）：进子游戏 → 大厅 Web `onInactive()` **彻底停网络+渲染**；返回 → 销毁子游戏 Web、大厅 `onActive()` 恢复（返回快、保留大厅状态/连接）。
> - **切换拓扑**：仅 大厅↔子游戏，**无 子游戏→子游戏**（只需一个子游戏槽）。
> - **各自独立桥+能力**：大厅 Web 与子游戏 Web 各持一套 `BridgeController` + `buildCapabilities()` 注册；激活者响应。
> - 注：`onInactive()` 暂停 JS/渲染/多数活动；H5 自开的 WebSocket 在原生层未必完全断（H5 零改动下最接近的原生手段）。
> - M2/M4 的单 WebView `loadUrl` 切换是过渡实现，M5 用 `NodeController`/`BuilderNode` 离屏保活大厅 + 临时子游戏 Web 落地此模型。

### 7.2 GenericWebContainer（通用网页容器）

对应 `openwebActivity1`，**不挂桥**，用 `javaScriptProxy` 注入 `settings` 对象 + `runJavaScript` 直调。

```typescript
@Component
export struct GenericWebContainer {
  private controller = new webview.WebviewController();
  // settings：H5→原生 6 方法（《契约规范》§11.3）
  private settings = new SettingsProxy(this.controller, /*onReturnData*/ (data)=> this.finishWith101(data));

  build() {
    Web({ src: this.params.url, controller: this.controller })
      .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
      .geolocationAccess(true).zoomAccess(false)
      .cacheMode(this.online ? CacheMode.Default : CacheMode.Only)    // 有网:按HTTP缓存策略(≈LOAD_DEFAULT)；无网:只用缓存(≈LOAD_CACHE_ELSE_NETWORK)。非关键项
      .javaScriptProxy({
        object: this.settings, name: 'settings',
        methodList: ['backgameData','loadurl','browser','finishweb','isexitdialogeshow','isbackfinishweb'],
        controller: this.controller,
      })
      .onProgressChange((e) => { if (e.newProgress === 100 && !this.loaded) {
          this.loaded = true;
          this.controller.runJavaScript(`getWebdata('${this.params.data}')`); } })   // 原生→H5 直调
  }
  // 返回键：若 H5 调过 isbackfinishweb() → runJavaScript('gamebackkeydown()')；退出确认 → runJavaScript('backgameData()')
  private finishWith101(data: string) { /* 把 data 经路由/emitter 回传上层 Bridge 容器 → 上层 callHandler('getWebdata', data) */ }
}
```

- `settings` 的 6 方法、`getWebdata`/`gamebackkeydown`/`backgameData` 三个直调函数严格照《契约规范》§11.3。
- **回传上层**：用路由返回值或 `emitter` 事件把 `data` 投递回打开它的 Bridge 容器，由后者 `callHandler('getWebdata', data)`（复刻 Android 结果码 101 语义）。

### 7.3 file:// 跨域（V2 已验证，2026-06-25）

H5 以 `file://` 加载并请求本地资源时，ArkWeb 默认按 CORS 拦截（file 页面 origin 为 `null`，且 `file` 不在允许跨域的 scheme 列表）——**T-M2-08 真机首测确认了这一拦截**（`Access to fetch ... blocked by CORS policy`）。

**采用的解决方案（落地、对 H5 透明）**：在 `onControllerAttached` 调用
`WebviewController.setPathAllowingUniversalAccess([<filesDir>/tsgames])`，把资源根加入"允许跨域访问"白名单——该路径下的 `file://` 资源即放开同源限制，H5 的 XHR/fetch 正常工作。约束：路径须为 `filesDir`/`resourceDir` 子目录、与用户文件隔离；一旦设置，`file` 协议仅限访问白名单内资源（覆盖 `fileAccess` 行为）。

> 此方案比通用的 `onInterceptRequest`/`WebSchemeHandler` 代理更简（无需逐请求拦截、无需改首页 scheme），是 HarmonyOS 官方《Web 页面跨域解决方案·本地资源跨域》的推荐做法。若未来遇白名单不可用的边缘场景，仍可回退 `onInterceptRequest` 自定义响应。**对 H5 始终零改动**。

### 7.4 屏幕方向（横屏项目）

本项目为**横屏项目**：应用**默认横屏、不跟随设备传感器自动旋转**；同时**遵守 H5 零改动铁律**，完整保留 H5 经方向接口显式切换（含竖屏）的能力，与原 Android 一致（《契约规范》§7 屏幕方向约束 / §8.4 / §11）。落地方式：

- **横屏自动旋转**：`EntryAbility` 在 `module.json5` 配 `"orientation": "auto_rotation_landscape"`（在横屏两朝向间跟随重力传感器自动旋转，但不进入竖屏）。
- **H5 动态切换（保留竖屏能力）**：方向接口收到请求时用窗口 API **动态覆盖**，不写死锁定——
  - `AppSystemProvider` 的 `orientation` handler：`data === '1'` → 横屏，否则 → 竖屏（对齐 Android `setRequestedOrientation`）；
  - 子游戏 `SwitchOverGameData`：按 `webtype`（`"3"` 横 / `"2"` 竖）设方向后再 `loadUrl`；
  - `GenericWebContainer`：按打开参数 `orientation`（`"1"` 竖 / 其他 横）设方向。
- **实现 API**：运行时 `window.Window.setPreferredOrientation(orientation: window.Orientation)` 动态切换，`window.Orientation` 取 `LANDSCAPE` / `PORTRAIT`（编码前用 `devecocli docs` 核对签名）。
- **要点**：`module.json5` 的 `auto_rotation_landscape` 提供"横屏内跟随传感器旋转（不进竖屏）"，运行时 `setPreferredOrientation` 提供"H5 显式切换"——二者叠加既满足横屏项目定位，又守住 H5 零改动。

### 7.5 双 Web 大厅/子游戏架构（M5 定稿，2026-06-25 验证通过）

> 复刻原 Android 双 Activity 语义（webviewActivity 大厅 + NewwebviewActivity 子游戏，靠 Activity 生命周期 onPause/onResume 暂停/恢复），在鸿蒙用**单 Ability + 单页内两个 Web 组件**实现——更轻、切换更快、可离屏预热。M2/M4 的单 WebView `loadUrl` 切换是过渡实现，M5 替换之。

**结构**：`BridgeGameContainer` 内 `Stack { 大厅Web(常驻) ; if(子游戏!=null) 子游戏Web }`，由 **WebSlotManager** 持两 slot、维护 activeSlot、执行激活协议。每个 slot = 独立 `WebviewController + BridgeController + buildCapabilities()`。子游戏 `SwitchOverGameData` 创建、`backgameData` 销毁（不保活）。大厅常驻无需 NodeController（两 Web 同在一页）。

**需求①——大厅/子游戏接口不串（结构隔离）**：每个 Web 一套独立桥 + 能力实例，`BridgeController#L` 只 runJavaScript 到大厅 Web、`#S` 只到子游戏 Web，**物理不可能串**。唯一共享单例（`WeChatApi`）与 EventBus 导航事件由 WebSlotManager **路由到 activeSlot**。

**需求②——同时只有一个激活**（激活=联网+渲染+声音+桥接口可执行）。激活协议四联动：

| 维度 | 激活端 | 非激活端 |
|---|---|---|
| 渲染/动画/定位 | `web.onActive()` + 显示 | `web.onInactive()` + 隐藏 |
| 桥接口 | `bridge.setActive(true)` | `bridge.setActive(false)`：入站忽略、出站抑制 |
| 原生能力监听/播放 | `registrar.forEachForeground()` | `registrar.forEachBackground()`：停传感器/定位/网络监听、停音频 |
| H5 自身联网/心跳/声音 | `callHandler('appservice','1')` | `callHandler('appservice','2')`：H5 按契约自行进入后台态 |

> ⚠ **平台限制与正解**：ArkWeb `onInactive()` 暂停动画/定位但**不暂停 JS**；能停 JS 定时器的 `pauseAllTimers()` 是**全局**（会停两个 Web，不可用于单 Web）。即鸿蒙**没有"只冻结一个 Web 的 JS/网络"的原生开关**（比 Android WebView.onPause 弱）。正解：原生侧停掉能停的（渲染/能力/桥），**H5 侧用契约现成的 `appservice('2')` 自停网络/声音**——与原 Android 一致、H5 零改动。

**需求③——cookie + localStorage 共享（已真机验证）**：
- **localStorage**：✅ **已验证共享**——两个不同 file:// 路径（`filesDir/tsgames/lsA` 与 `/lsB`）+ `setPathAllowingUniversalAccess([filesDir/tsgames])`，一页写、另一页读到同值。故**沿用 file:// 主选**即满足。
- **cookie**：file:// 页面不支持 WebView cookie（cookie 绑定 http(s) scheme）；但原 Android 的 cookie 全在 native HTTP 层（Volley/java.net.CookieManager），**WebView 从未开 file-scheme cookie，H5 不用 WebView cookie**——故非需求缺口。若未来 H5 在 http(s) 用 cookie，`WebCookieManager` 为**应用级全局单例**（所有 Web 组件共享）自动满足。
- 结论：**采用 file:// + setPathAllowingUniversalAccess + WebCookieManager(全局)**，无需自定义协议。

**激活状态机**（WebSlotManager）：
```
进子游戏: deactivate(大厅)[appservice'2'→onInactive→forEachBackground→setActive(false)→隐藏]
        → 注入子游戏 app_data(launchtype'1') → 建子游戏 slot → activate(子游戏)[显示→onActive→forEachForeground→setActive(true)→100%时 appservice'1'+setPostUrl]
返回大厅: destroy(子游戏)[forEachBackground→bridge.dispose→销毁 Web] → activate(大厅)[显示→onActive→forEachForeground→setActive(true)→appservice'1'+getWebdata(data)]
App 前后台: 只对 activeSlot 下发 appservice
```

**改造清单**：新增 `WebSlotManager`(entry)；`BridgeController` 加 `setActive()`；`BridgeGameContainer` 改 Stack 双 Web、SWITCH_GAME/BACK_GAME 走 SlotManager；Provider 落实 `onForeground/onBackground`（Shake/Location/Network 停起监听、Audio 停起播放）；`WeChatApi` resp 路由到 activeSlot。

---

## 8. 启动编排与配置/资源子系统

### 8.1 StartupOrchestrator（启动状态机）

把《契约规范》§3 时序实现为**显式状态机**，每步可重试、可观测、可降级：

```
INIT → LOAD_LOCAL_CONFIG → REQUEST_PERMISSIONS → FETCH_REMOTE_CONFIG
     → RESOLVE_VERSION → [PREPARE_RESOURCE: 内置拷贝/zip下载解压] → INJECT_APP_DATA
     → ENTER_HALL (跳 BridgeGameContainer)
   任一步失败 → FALLBACK(本地缓存) 或 BLOCK(showmessage 公告)
```

- `showmessage` 非空 → 阻断弹窗（§3.2）。
- 远程配置长度 <30 视为错误文案直接提示。

### 8.2 ConfigManager（配置）

- **本地配置**：弃用"目录名编码"，改为随包内置 `resources/rawfile/app_config.json`（KV），提供《契约规范》§4.2 全部键：`agent/channel/gamedir/gamestart/appversion/market/gameid/weburl/gameconfig/other/tuiguang`。对 H5 无感知。
- **远程配置**：`HttpClient.get("http://"+gameconfig.replace(/-/g,'/')+".txt?a="+ts)`，禁缓存。
- **分层覆盖**：`VersionResolver` 按线上真实格式实现 **agent→game→channel→market** 后层覆盖（§4.4）——单 `agentlist` 树、资源字段 `game_zip`、数字 id/版本归一、gameid 为空回退 `version.xml` 的 `<game id>`，无二级 `url` 二次请求。纯函数实现，便于单测。

### 8.3 ResourceManager（资源）

```
路径（@ohos.file.fs + context.filesDir）：
  解压根 urlpath = <filesDir>/tsgames/<bundle>/<时间戳>/<gamedir>
  游戏目录       = <urlpath>/gamehall
  入口           = file://<urlpath>/gamehall/index.html
  KV 持久化       urlpath / upurlpath（@ohos.data.preferences）
流程：
  首启 → 拷贝内置包(rawfile) → @ohos.zlib.decompressFile 解压
  非首启 → 比较 version.xml<version value> 与远程 game_version
  需更新 → Downloader 下载 game_zip(?a=ts) → 删旧 → decompressFile → 收尾
```

- 下载、解压在 **TaskPool/Worker** 执行（§9），UI 线程只收进度回调。
- `version.xml` 解析用 `@ohos.xml`（XmlPullParser），取 `agent/game/channel/version`。

### 8.4 AppDataInjector

加载大厅前，在 `<urlpath>/gamehall/app_data.js` 写入《契约规范》§6 的全局变量（`app_version/app_agent/app_market/...`）。用 `fs` 写文件即可；**注意必须在 `Web` 加载该页面之前完成**（在 StartupOrchestrator 的 INJECT_APP_DATA 步，先于 ENTER_HALL）。

---

## 9. 性能架构

> "高性能"落在四个可量化抓手上。

| 抓手 | 设计 | 收益 |
|---|---|---|
| **Web 保活 / 预热**（确认模型见 §7.1） | **双 Web 槽**：大厅 Web 常驻（`NodeController`/`BuilderNode` 离屏保活），子游戏 Web 进入创建/返回销毁；**同时只一个激活**——进子游戏时大厅 `onInactive()` 停网络+渲染，返回时大厅 `onActive()` 恢复并销毁子游戏 Web；各 Web 独立桥+能力；无子游戏→子游戏 | 子游戏进入<300ms、返回大厅秒回且保留状态/连接、非激活端不占网络/CPU |
| **并发卸载重活** | 下载/解压/解密/MD5 校验放 **TaskPool**（短任务）或 **Worker**（长驻）；UI 线程零阻塞 | 启动期不卡顿，进度流畅 |
| **JSBridge 批处理** | 沿用 lzyzsd 队列语义：多条出站消息可在一次 `flushMessageQueue` 内聚合；高频 handler（定位/电量）做节流 | 减少 `runJavaScript` 跨引擎调用次数 |
| **资源就绪即渲染** | 本地 `file://` 优先；首屏资源预解压；`onPageEnd` 后再注桥，避免阻塞首屏；图片/音频懒加载 | 首帧更快 |

补充：
- **内存**：容器 `aboutToDisappear` 解绑 Controller、`onRenderExited` 兜底重载（防渲染子进程崩溃白屏）。
- **冷启动**：`StartupOrchestrator` 并行化"权限申请"与"内置包拷贝"等无依赖步骤。
- **包体**：能力 Provider 按渠道裁剪（HSP 动态特性）。

### 9.1 TaskPool 并发的 Sendable 约束（P1，必须遵守）⚠️

HarmonyOS `TaskPool` 的 `@Concurrent` 任务**跨线程传参/返回值必须是 Sendable**（基本类型、`@Sendable` class、可序列化对象），**不能传**：闭包回调、`WebviewController`、`UIAbilityContext`、复杂业务对象。因此下载/解压的"进度回传"不能直接传 callback 进 TaskPool。落地范式：

```typescript
// 1) 重活在 @Concurrent 函数里跑，只接收/返回 Sendable 数据
@Concurrent
function unzipTask(zipPath: string, destDir: string): boolean { /* @ohos.zlib.decompressFile */ return true; }

// 2) 进度/完成经 emitter 回 UI 线程（不跨线程传函数）
@Concurrent
function downloadTask(url: string, savePath: string, eventId: number): void {
  // 边下边 emitter.emit({eventId}, { data: percent })  ← 仅传 Sendable 数字
}
// UI 线程：emitter.on({eventId}, e => updateProgressUI(e.data.percent));
//          taskpool.execute(downloadTask, url, savePath, eventId);
```

> 要点：**TaskPool 只搬 Sendable 数据，UI 反馈一律走 `emitter`/`AppStorage`**。这条不遵守，编译期/运行期必报错。`context.filesDir` 等路径应在 UI 线程取好后以字符串传入。

---

## 10. 横切关注点

| 关注点 | 设计 |
|---|---|
| **契约类型（SSOT）** | `contracts` HAR：`enum Handlers`（所有 handler 名常量，杜绝拼写漂移如 `finsh`/`getcameraaAddress`/`backgameData-`）；每个 DTO 一个 interface（§12 结构）；编译期保证 |
| **日志/追踪** | `Logger`（分级）+ `BridgeTracer`：每条桥消息打 `traceId`，可还原"H5调用→handler→回传"全链路 |
| **错误中心** | `Result<T>` 统一返回；`ErrorCenter` 收敛异常并按策略（弹窗/静默/上报） |
| **事件总线** | `EventBus`/`emitter`：前后台、网络变化、容器间回传等解耦 |
| **依赖注入** | 轻量 `DIContainer`（组装根 `AppModule` 集中装配），避免散落 new |
| **权限** | `PermissionGuard` 统一申请存储/电话/定位/相机/麦克风，对齐《契约规范》§13 |
| **安全** | 密钥下沉服务端；`file://` 跨域按官方安全实践；自定义协议白名单 |
| **可测试** | 桥引擎、VersionResolver、MessageCodec 纯逻辑可单测；能力 Provider 以 mock bridge 测注册与回传 |

---

## 11. 契约可追溯性矩阵

> 保证"设计覆盖契约 100%"，无遗漏。

| 契约规范条目 | 落地模块 | 验收点 |
|---|---|---|
| §2 桥协议（yy://、_handleMessageFromNative、_fetchQueue、Message、注入时机） | `feature_bridge/BridgeController + MessageCodec + WebViewJavascriptBridge.js` | H5 `registerHandler/callHandler` 全部生效 |
| §8 入站 44 handler | `feature_capabilities/*Provider.register()` | 逐一对照名称/参数/同步返回 |
| §9 出站 ~21 handler | 各 Provider 在事件时 `bridge.callHandler` | 名称/data 结构/时机一致 |
| §11.1 路径 A `SwitchOverGameData` | `BridgeGameContainer` + `NavProvider` | 同容器 loadUrl 切换 |
| §11.2/§11.3 通用网页容器 | `GenericWebContainer + SettingsProxy` | `settings` 6 方法 + 3 直调函数 + 101 回传 |
| §3 启动时序 | `StartupOrchestrator` | 状态机逐步 |
| §4 配置/分层覆盖 | `ConfigManager + VersionResolver` | 单 agentlist 树 · agent→game→channel→market 后层覆盖 · game_zip |
| §5 资源管理 | `ResourceManager + Unzipper + Downloader` | 路径/版本比较/下载解压 |
| §6 app_data.js | `AppDataInjector` | 全局变量同名同义、加载前注入 |
| §7 WebView 能力 | `BridgeGameContainer` 属性 | JS/DOM/file/mixed/cache/geo |
| §10 设备能力 | 各 Provider | 分享/登录/支付/定位/音频/摇一摇/扫码/相机/震动/剪贴板/网络/电量/WiFi/电话 |
| §13 常量/权限 | `ConfigManager + PermissionGuard` | 渠道账号、权限齐备 |
| §14.2 补齐项 | 新增/增强 Provider | QQ/抖音回调、抖音登录、主动截图、密钥下沉 |

---

## 12. 实施路线图（里程碑）

| 阶段 | 交付 | 关键验收 |
|---|---|---|
| **M0 脚手架** | `devecocli create` 多模块工程、CI、签名 | 空壳可跑 |
| **M1 桥引擎（最高优先）** | `feature_bridge` + `IWebController` + 线程守卫 + 回显 handler；注入桥 JS | ① 最小回显 H5：`callHandler('getTime')` 能返回；**② 阻塞性验证：`yy://` 的 iframe 导航确被 `onLoadIntercept` 捕获**（否则立即切 §5.1 回退方案） |
| **M2 容器 + 启动 + 资源** | 双容器、StartupOrchestrator、Config/Resource/AppData | 真机加载现有大厅 H5 `index.html`，能进子游戏；**阻塞性验证：`file://` 加载下 H5 的 XHR/fetch 取本地资源可用**（否则启用 §7.3 自定义协议方案） |
| **M3 能力（本期范围）** | 分享/登录/定位/音频/摇一摇/扫码/相机/震动/剪贴板/网络/设备 等 Provider；**视频房、支付用占位桩（§6.5）**；闲聊不涉及 | 用真实 H5 跑通本期业务；**视频房/支付被调用时无报错、无卡死**（桩 + 默认 handler 兜底）；逐条对照可追溯性矩阵 |
| **M4 通用网页容器** | `GenericWebContainer + SettingsProxy` | 活动页/收银台/客服打开、回传正常 |
| **M5 性能 & 加固** | Web 保活、TaskPool、密钥下沉、可观测 | 子游戏切换流畅、冷启动达标、安全审查通过 |
| **M6 暂缓能力集成（按需）** | 把 `RoomStubProvider`/`PayStubProvider` 换回真实 `RoomProvider`/`PayProvider`；如需再加抖音登录等补齐项 | 真实视频房/支付打通，**H5 仍零改动** |

> **强烈建议**：M1 桥引擎必须先用一个**最小回显 H5** 单独验证，再接真实 H5；桥不稳，一切白搭。**两个"阻塞性验证"（`yy://` 拦截、`file://` 跨域）必须在 M1/M2 通过**——它们是整个适配最底层的两个不确定性，越早证伪越好。

---

## 13. 验收标准

**功能验收（H5 零改动）**：
- [ ] 现有大厅 H5 与全部子游戏 H5 **一行不改**即可运行。
- [ ] 《契约规范》§8 全部入站 handler、§9 全部出站 handler 行为一致（名称/参数/数据结构/时机）。
- [ ] 通用网页容器 `settings` 6 方法 + 3 直调函数 + 101 回传一致。
- [ ] `app_data.js` 全局变量、入口 URL、`?Launchtype=` 参数一致。
- [ ] **暂缓能力（视频房 `createRoom`/`exitRoom`/`getVideoinfo`/`DragViewvideoIsshow`、支付 `paybrowser`/`getGameplay`）被 H5 调用时：不报错、不卡死、不误触发出站推送**（占位桩 + 默认 handler 兜底，§6.5）。

**质量验收**：
- [ ] 桥引擎、VersionResolver、MessageCodec 单测覆盖率 ≥80%。
- [ ] 冷启动到大厅首帧、子游戏切换时延达标（建议 <2s / <300ms，按机型基线）。
- [ ] 下载/解压全程不阻塞 UI；渲染崩溃可自恢复。
- [ ] 无客户端硬编码密钥；权限按需申请、合规。
- [ ] 全链路日志可追踪一条桥消息。

---

> **一句话总结**：以 `feature_bridge`（100% 复刻 lzyzsd 协议）为基石，能力以 `CapabilityProvider` 插件化挂载，配置/资源/启动以领域服务编排，双容器分别承载"桥协议"与"settings 注入"两套契约——这套现代化 ArkTS 分层框架能在不改任何 H5 的前提下，优雅、高效地承载 TSGame 全部业务，并为后续能力补齐与多渠道演进留足空间。

---

## 附录 A：ArkTS / ArkUI 实现适配清单

> 前文为"设计级"。本附录把设计落到 ArkTS/ArkUI 的**现实工程约束**上，照此即可从"设计就绪"过渡到"编码就绪"。

### A.1 ArkUI `@Component struct` 与桥/控制器的协作
- **持有方式**：`WebviewController` 与 `BridgeController` 作为 struct 的**普通成员变量**持有即可（非响应式，不要加 `@State`；它们的变化不应驱动 UI 重渲染）。需要驱动 UI 的状态（加载进度、loading 显隐）才用 `@State`/`@Local`。
- **注入时机**：`onControllerAttached` 回调里才能安全调用 Controller 相关接口；**能力注册（`provider.register`）放这里**。`aboutToAppear` 只做无 Controller 依赖的设置（如 debug 开关）。
- **生命周期解绑**：`aboutToDisappear` 中停止能力（`provider.onDestroy`）、清空 `responseCallbacks`/`registry`、解绑 Controller，防泄漏（对应 Android `onDestroy` 的 WebView 清理）。
- **前后台**：在 `UIAbility.onForeground/onBackground` 或页面 `onPageShow/onPageHide` 调 `provider.onForeground/onBackground` 并 `callHandler('appservice','1'|'2')`。

### A.2 ArkTS 严格语法注意
- 全量类型标注，**禁用 `any`**；DTO 用 `interface`，handler 名用 `enum`/`const`（`contracts` HAR）。
- 闭包/箭头函数可用，但**跨线程（TaskPool）的函数须 `@Concurrent` 且仅收发 Sendable**（见 §9.1）。
- 动态对象访问受限：解析 H5 传入 JSON 后，**显式映射到 DTO**，不要依赖结构化鸭子类型。
- 单例/DI：用模块级实例或轻量 `DIContainer`；避免在 struct 内散落 `new` 业务对象。

### A.3 线程与并发（呼应 §5.5 / §9.1）
| 场景 | 线程要求 | 手段 |
|---|---|---|
| `runJavaScript` 下发出站消息 | **必须 UI 线程** | `BridgeController.dispatch` 内置 UI 线程守卫 |
| 能力异步回调（定位/支付/传感器/广播） | 任意线程 → 切 UI | `emitter`/UIContext 任务回 UI 线程再 `callHandler` |
| 下载/解压/MD5/解密 | 子线程 | `TaskPool @Concurrent`，仅传 Sendable，进度经 `emitter` |
| 大文件 IO | 子线程 | `@ohos.file.fs` 异步 API |

### A.4 关键系统能力对照（编码备查）
| 用途 | 模块 |
|---|---|
| Web 组件/控制器 | `@kit.ArkWeb`（`webview.WebviewController`、`Web`） |
| 偏好存储（urlpath 等） | `@ohos.data.preferences` |
| 文件 IO | `@ohos.file.fs` |
| 解压 | `@ohos.zlib`（`decompressFile`） |
| XML（version.xml） | `@ohos.xml`（`XmlPullParser`） |
| 网络请求/下载 | `@ohos.net.http` 或 `@kit.RemoteCommunicationKit`（`rcp`） |
| 本机端口服务（截图上传） | `@ohos.net.socket`(TCP) 自建 / NAPI 原生 server |
| 并发 | `@ohos.taskpool` / `Worker` |
| 事件 | `@ohos.events.emitter` |
| 定位 | `@ohos.geoLocationManager` |
| 电量/WiFi/网络 | `@ohos.batteryInfo` / `@ohos.wifiManager` / `@kit.NetworkKit` |
| 设备/电话 | `@ohos.deviceInfo` / `@kit.TelephonyKit` |
| 扫码/相机 | Scan Kit / Camera Picker |
| 震动/传感器（摇一摇） | `@ohos.vibrator` / `@ohos.sensor` |
| 剪贴板 | `@ohos.pasteboard` |
| 音频 | `@kit.MediaKit`（`AVPlayer`/`SoundPool`） |

> 注：具体 API 名以目标 SDK 版本为准，编码前用 `devecocli docs` 核对签名。

---

## 附录 B：生产加固与合规清单

| 项 | 要求 |
|---|---|
| **远程调试** | `setWebDebuggingAccess(true)` **仅 debug 包**（`BuildProfile.DEBUG` 守卫）；release 必关 |
| **密钥** | 微信 `AppSecret`/支付 `API_KEY` **不入客户端**；登录走 code、支付签名走服务端（《契约规范》§13） |
| **明文 HTTP** | 远程配置/资源走 http 需在 `module.json5` 网络安全配置中显式放行域名；逐步迁 https |
| **file:// 跨域** | 用官方方案（自定义协议/`onInterceptRequest`）而非放开全局，最小授权（§7.3） |
| **自定义 scheme 注册** | `gamepaywelcome` 等外部唤起在 `module.json5` 的 `abilities.skills.uris` 声明 |
| **权限合规** | 定位/相机/麦克风/电话**用时申请 + 文案说明**；高德等 SDK 隐私合规初始化（`updatePrivacyShow/Agree`）先于使用 |
| **渲染兜底** | `onRenderExited` 重载页面，防子进程崩溃白屏 |
| **数据安全** | KV/文件按需加密；日志脱敏（不打 openid/token 明文） |
| **上架审核** | 准备隐私声明、权限用途清单，符合 HarmonyOS 应用市场审核要求 |
