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:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user