review: M2 成果审查后的质量加固
三路+对照审查 M2 全部成果。桥核心(M1,本期未改)与契约骨架零问题;发现集中在新的资源/启动/容器代码,已修: 高危 - ResourceManager.updateGame 原子化:下载→解压暂存→校验 <gamestart>/index.html→才删旧并 rename 换入。 修复"先删旧 gamehall 再解压,失败即把可用资源砖掉"且 catch『keep local』失效的数据丢失风险 - BridgeGameContainer.setupBridge:先建桥并赋值 this.bridge,能力注册/配置加载整体兜底 try/catch。 修复"config/registration 抛错→bridge 永远 undefined→H5 所有 callHandler 静默"违反 H5 零改动铁律 中危 - VersionResolver:空 gameid 守卫(大厅不参与 game 树匹配,避免误命中 gamelist 空 id 节点); 两树 version 平手时取"有下载地址"的一棵(避免选中只有版本号无下载地址的层)。新增 2 单测,覆盖率 83.7% - Downloader:dataReceive 写盘失败标记并于请求结束后抛出(避免磁盘满致截断文件被当作下载成功) - FileSystem:rmrf 的 statSync 纳入 try(兑现"不存在/竞态则忽略"承诺);copyRawFileTo 按 byteOffset/byteLength 切片写入(避免 Uint8Array 为大池子视图时写入多余字节致 zip 损坏);新增 rename - ConfigManager.fetchSecondary:二级节点本层版本字段也并入(此前只换 channellist/agentlist 会丢节点自身版本/下载地址) 清理/抗回归 - 跨域白名单路径改用 ResourceManager.resourceRoot()(单一来源,避免与领域层路径定义漂移致 V2 回归) - 前后台订阅移到 setupBridge(桥就位后),避免冷启动 FOREGROUND 早于桥到达被丢 - prepareBuiltin 单次读取内置包(去掉"先整包读探测、再整包读拷贝"双读) - LocalUploadServer 删死码 isRunning/running - 文档化延期项:weburl 分支不注入 app_data(同 Android,远程大厅自带)、app_gamename 待 M3 扩展 VersionXml、 子游戏切换 firstProgressDone 复位 TODO(M3 T-M3-06) devecocli build 通过;单测全通过;模拟器复测 app_data/V2 fetch/桥双向/特殊字符往返均无回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { webview } from '@kit.ArkWeb';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
||||
import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities';
|
||||
import { ConfigManager } from 'domain_resource';
|
||||
import { ConfigManager, ResourceManager } from 'domain_resource';
|
||||
import { KvStore, LocalUploadServer } from 'platform';
|
||||
import { OutboundHandlers } from 'contracts';
|
||||
import { AppEnv, EventBus, Logger } from 'common';
|
||||
@@ -33,7 +33,11 @@ export struct BridgeGameContainer {
|
||||
if (AppEnv.isDebug()) {
|
||||
webview.WebviewController.setWebDebuggingAccess(true);
|
||||
}
|
||||
// 前后台联动:EntryAbility 经 EventBus 广播 → 转给各能力 + callHandler('appservice')
|
||||
// 前后台订阅移到 setupBridge(桥就位后),避免冷启动 FOREGROUND 早于桥到达被静默丢弃
|
||||
}
|
||||
|
||||
/** 前后台联动订阅:EntryAbility 经 EventBus 广播 → 转给各能力 + callHandler('appservice')。 */
|
||||
private subscribeLifecycle(): void {
|
||||
this.cancelForeground = EventBus.on(AppEvents.FOREGROUND, () => {
|
||||
this.registrar?.forEachForeground();
|
||||
this.bridge?.callHandler(OutboundHandlers.AppService, '1');
|
||||
@@ -68,30 +72,39 @@ export struct BridgeGameContainer {
|
||||
// 🔴 V2 file:// 跨域(框架 §7.3):file:// 页面的 XHR/fetch 默认被 CORS 拦截(origin 'null')。
|
||||
// 官方方案:把资源根加入"允许跨域访问"白名单,file:// 访问该路径下资源即放开同源限制。
|
||||
// 对 H5 完全透明(仍 file:// 加载)。路径须在 filesDir 子目录且与用户文件隔离。
|
||||
// 跨域白名单路径取自 ResourceManager(单一来源,避免与领域层路径定义漂移致 V2 回归)
|
||||
try {
|
||||
this.controller.setPathAllowingUniversalAccess([`${hostCtx.filesDir}/tsgames`]);
|
||||
this.controller.setPathAllowingUniversalAccess([ResourceManager.resourceRoot(hostCtx)]);
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
Logger.tag('BridgeGameContainer').e(`setPathAllowingUniversalAccess failed: ${err.message}`);
|
||||
}
|
||||
|
||||
// 🔴 先建桥并就位:即使下面能力注册/配置加载抛错,桥仍可用——未注册 handler 落 DefaultHandler,
|
||||
// H5 调用不报错(铁律最后防线)。桥失效才是真正违反"H5 调用永不报错"。
|
||||
const adapter = new WebviewControllerAdapter(this.controller);
|
||||
const bridge = new BridgeController(adapter);
|
||||
bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
|
||||
|
||||
const ctx: CapabilityContext = {
|
||||
uiAbilityContext: hostCtx,
|
||||
config: ConfigManager.load(hostCtx),
|
||||
kv: KvStore.create(hostCtx),
|
||||
uploadServer: this.uploadServer,
|
||||
log: Logger.tag('Capability'),
|
||||
};
|
||||
const registrar = new CapabilityRegistrar(buildCapabilities());
|
||||
registrar.registerAll(bridge, ctx);
|
||||
|
||||
this.adapter = adapter;
|
||||
this.bridge = bridge;
|
||||
this.registrar = registrar;
|
||||
|
||||
try {
|
||||
bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
|
||||
const ctx: CapabilityContext = {
|
||||
uiAbilityContext: hostCtx,
|
||||
config: ConfigManager.load(hostCtx),
|
||||
kv: KvStore.create(hostCtx),
|
||||
uploadServer: this.uploadServer,
|
||||
log: Logger.tag('Capability'),
|
||||
};
|
||||
const registrar = new CapabilityRegistrar(buildCapabilities());
|
||||
registrar.registerAll(bridge, ctx);
|
||||
this.registrar = registrar;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
Logger.tag('BridgeGameContainer').e(`capability registration failed (bridge still usable): ${err.message}`);
|
||||
}
|
||||
|
||||
this.subscribeLifecycle();
|
||||
this.uploadServer.start();
|
||||
}
|
||||
|
||||
@@ -124,7 +137,10 @@ export struct BridgeGameContainer {
|
||||
this.bridge?.onPageEnd();
|
||||
})
|
||||
.onProgressChange((event: OnProgressChangeEvent) => {
|
||||
// 首次进度 100% → 推 appservice(前台) + setPostUrl(与 Android onProgressChanged==100 一致)
|
||||
// 首次进度 100% → 推 appservice(前台) + setPostUrl(与 Android onProgressChanged==100 一致)。
|
||||
// TODO(M3 T-M3-06 子游戏切换):SwitchOverGameData 走同容器 controller.loadUrl 后页面会再次到 100%,
|
||||
// 需在 loadUrl 前重置 firstProgressDone 并重注入 app_data.js,使切换后的子游戏页同样收到 setPostUrl。
|
||||
// 届时新增 reloadGame(entryUrl) 入口由 NavProvider 调用;当前 M2 仅首屏,故一次性标志足够。
|
||||
if (event.newProgress === 100 && !this.firstProgressDone) {
|
||||
this.firstProgressDone = true;
|
||||
const b = this.bridge;
|
||||
|
||||
@@ -111,5 +111,33 @@ export default function versionResolverTest() {
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, keys);
|
||||
expect(d.gameDownload).assertEqual('keep');
|
||||
});
|
||||
|
||||
it('empty_gameid_skips_game_tree', 0, () => {
|
||||
// 大厅 gameid='' 不应误命中 gamelist 中 gameid 为空的节点
|
||||
const lobbyKeys: MatchKeys = { agentid: 'A1', channelid: 'C1', marketid: 'M1', gameid: '' };
|
||||
const remote: RemoteConfig = {
|
||||
gamelist: [{ gameid: '', game_version: '99', game_download: 'should_not_match',
|
||||
agentlist: [{ agentid: 'A1', game_version: '99', game_download: 'should_not_match' }] }],
|
||||
agentlist: [{ agentid: 'A1', game_version: '10', game_download: 'agent_only',
|
||||
channellist: [{ channelid: 'C1',
|
||||
marketlist: [{ marketid: 'M1',
|
||||
gamelist: [{ gameid: '', game_version: '88', game_download: 'should_not_match' }] }] }] }],
|
||||
};
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, lobbyKeys);
|
||||
expect(d.gameVersion).assertEqual(10);
|
||||
expect(d.gameDownload).assertEqual('agent_only');
|
||||
});
|
||||
|
||||
it('tie_prefers_nonempty_download', 0, () => {
|
||||
// 两树 game_version 相等(20),代理树只有版本号无下载地址 → 应取游戏树的下载地址
|
||||
const remote: RemoteConfig = {
|
||||
agentlist: [{ agentid: 'A1', game_version: '20' }],
|
||||
gamelist: [{ gameid: 'G1',
|
||||
agentlist: [{ agentid: 'A1', game_version: '20', game_download: 'game_url' }] }],
|
||||
};
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, keys);
|
||||
expect(d.gameVersion).assertEqual(20);
|
||||
expect(d.gameDownload).assertEqual('game_url');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user