M2(领域层): ConfigManager/VersionResolver/ResourceManager/AppDataInjector/StartupOrchestrator

T-M2-05 ConfigManager:本地 rawfile/app_config.json(§4.2 实测值)、远程 URL 拼装('-'→'/'+.txt+?a=ts 禁缓存)、
        长度<30 走 showmessage 阻断、二级 url 二次请求(语义待真机校验)、产出版本决策
T-M2-06 VersionResolver:纯函数,agentlist/gamelist 两棵树 + agent→channel→market→game 后层覆盖 +
        两树取更高 version(§4.4)。单测 8 用例全通过,覆盖率 81.8%(≥80%)
T-M2-07 ResourceManager:沙箱路径规范、内置包拷贝解压、version.xml 解析(@ohos.xml)、远程 zip 下载/删旧/解压、版本比较
T-M2-09 AppDataInjector:加载前写 <gameDir>/app_data.js(§6 全局变量同名同义,JSON.stringify 安全字面量)
T-M2-11 StartupOrchestrator:INIT→…→ENTER_HALL 状态机,远程失败降级本地、showmessage 阻断、weburl 空/非空入口规则
配套:module.json5 声明 ohos.permission.INTERNET + reason_internet 字符串

devecocli build 通过;scripts/test.sh 单测全通过(修复 PermissionRequestResult 顶层导入)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-25 18:29:45 +08:00
parent ecb0e4642a
commit d2da223c8d
15 changed files with 953 additions and 5 deletions
@@ -0,0 +1,127 @@
/**
* 资源管理(契约 §5,框架 §8.3)。负责大厅/子游戏资源的落盘与版本管理:
* - 路径规范(沙箱 filesDir)。
* - 首启:拷贝内置预置包(rawfile zip) → 解压。
* - 版本:解析本地 version.xml,与远程 game_version 比较。
* - 更新:下载远程 zip → 删旧 gamehall → 解压 → 收尾。
* - 持久化 urlpath/upurlpath 到 KvStore(供容器读取)。
*
* 沙箱简化(§5.1 许可):弃用 Android 的"<包名>/<时间戳>"多级目录,用固定目录,
* 对 H5 无影响(H5 只看到自己被 file:// 加载)。
*/
import { common } from '@kit.AbilityKit';
import { Downloader, FileSystem, KvStore, ProgressCallback, Unzipper } from 'platform';
import { Logger } from 'common';
import { AppConfig } from '../config/AppConfig';
import { VersionXml } from './VersionXml';
/** 资源路径集合。 */
export interface ResourcePaths {
/** 父目录(upurlpath)。 */
upurlpath: string;
/** 解压根(urlpath)。 */
urlpath: string;
/** 游戏目录(<urlpath>/<gamestart>,即 gamehall)。 */
gameDir: string;
/** 大厅入口文件磁盘路径。 */
indexPath: string;
/** 大厅入口 file:// URL(不含 ?Launchtype)。 */
indexUrl: string;
}
/** KvStore 键。 */
const KEY_URLPATH: string = 'urlpath';
const KEY_UPURLPATH: string = 'upurlpath';
/** 内置预置包 rawfile 名(可选;缺失则依赖远程下载)。 */
const BUILTIN_ZIP_RAWFILE: string = 'gamehall_builtin.zip';
export class ResourceManager {
private static readonly log: Logger = Logger.tag('ResourceManager');
private readonly context: common.Context;
private readonly config: AppConfig;
private readonly kv: KvStore;
private readonly cachedPaths: ResourcePaths;
constructor(context: common.Context, config: AppConfig, kv: KvStore) {
this.context = context;
this.config = config;
this.kv = kv;
this.cachedPaths = this.computePaths();
}
private computePaths(): ResourcePaths {
const upurlpath: string = `${this.context.filesDir}/tsgames`;
const urlpath: string = `${upurlpath}/${this.config.gamedir}`;
const gameDir: string = `${urlpath}/${this.config.gamestart}`;
const indexPath: string = `${gameDir}/index.html`;
return { upurlpath, urlpath, gameDir, indexPath, indexUrl: `file://${indexPath}` };
}
paths(): ResourcePaths {
return this.cachedPaths;
}
/** 大厅入口是否已就绪(index.html 存在)。 */
isPrepared(): boolean {
return FileSystem.exists(this.cachedPaths.indexPath);
}
/** 本地资源版本(version.xml 的 version;缺失返回 0)。 */
localVersion(): number {
const candidates: string[] = [
`${this.cachedPaths.gameDir}/version.xml`,
`${this.cachedPaths.urlpath}/version.xml`,
];
for (const p of candidates) {
if (FileSystem.exists(p)) {
return VersionXml.parseVersion(FileSystem.readText(p));
}
}
return 0;
}
/** 持久化路径到 KvStore(供容器/重启读取)。 */
async persistPaths(): Promise<void> {
this.kv.putString(KEY_URLPATH, this.cachedPaths.urlpath);
this.kv.putString(KEY_UPURLPATH, this.cachedPaths.upurlpath);
await this.kv.flush();
}
/**
* 首启拷贝内置预置包并解压(若 rawfile 存在)。
* 内置包顶层即含 gamehall/...,解压到 urlpath 即可。无内置包则跳过(依赖远程下载)。
*/
async prepareBuiltin(): Promise<void> {
let bytesAvailable: boolean = true;
try {
this.context.resourceManager.getRawFileContentSync(BUILTIN_ZIP_RAWFILE);
} catch (e) {
bytesAvailable = false;
}
if (!bytesAvailable) {
ResourceManager.log.w(`no builtin package (${BUILTIN_ZIP_RAWFILE}); rely on remote download`);
return;
}
const tmpZip: string = `${this.cachedPaths.upurlpath}/builtin.zip`;
FileSystem.copyRawFileTo(this.context.resourceManager, BUILTIN_ZIP_RAWFILE, tmpZip);
FileSystem.ensureDir(this.cachedPaths.urlpath);
await Unzipper.unzip(tmpZip, this.cachedPaths.urlpath);
FileSystem.rmrf(tmpZip);
ResourceManager.log.i('builtin package prepared');
}
/**
* 从远程下载 zip 并更新游戏资源(契约 §5.4):
* 下载 game_download(?a=ts) → 删旧 gamehall → 解压到 urlpath → 删 zip。
*/
async updateGame(downloadUrl: string, onProgress?: ProgressCallback): Promise<void> {
const url: string = downloadUrl.includes('?') ? downloadUrl : `${downloadUrl}?a=${Date.now()}`;
const zipPath: string = `${this.cachedPaths.upurlpath}/dowlod_zip/${Date.now()}Projects.zip`;
await Downloader.download(url, zipPath, onProgress);
FileSystem.rmrf(this.cachedPaths.gameDir);
FileSystem.ensureDir(this.cachedPaths.urlpath);
await Unzipper.unzip(zipPath, this.cachedPaths.urlpath);
FileSystem.rmrf(zipPath);
ResourceManager.log.i(`game updated from ${url}`);
}
}