修复子游戏/大厅每次启动都重复下载:version.xml 改用正则解析

现象:子游戏 zip 已下载解压、version.xml 落盘正常,但再次进入仍重新下载;
远程版本与本地一致本应跳过下载。真机诊断([VERXML-DIAG]/[SUBGAME-DIAG]):
installed=true、version.xml 原文完整(<version value="155"/>)、gameId 正确解析为
a52y...,唯独 localVer 恒为 0 → 155<=0 判否 → 每次都判需更新而重下。大厅同理
(localVersion 恒 0,启动期每次重下整包)。

根因:VersionXml.parse 用 @kit.ArkTS xml.XmlPullParser,其
attributeValueCallbackFunction 对本文件 <version> 的 value 属性不稳定触发(同文件
<game> 的 id 却能取到),逻辑自相矛盾——value 无条件捕获却落空,说明该属性回调
未按预期触发。

修法:弃用 XmlPullParser,改确定性正则直接从原文抽 version/gameId/gameName,语义
等价 Android pullxml1,规避解析器回调时序/属性触发的不确定性。根 <game> 无属性
(紧跟 '>'),\s+ 自然跳过,只命中带属性的子 game。

验证:真机覆盖安装后再次进入三个老K,localVer=155、不再下载,直接进 H5。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 09:18:48 +08:00
co-authored by Claude Opus 4.8
parent db206068c0
commit 600378f723
@@ -1,15 +1,15 @@
/**
* version.xml 解析(契约 §5.3,框架 §8.3)。用 @kit.ArkTS xml.XmlPullParser。
* version.xml 解析(契约 §5.3,框架 §8.3)。
*
* 结构:<game><agent id name/><game id name/><channel id name/><version value name/></game>
* 用途:取 <version value="42"> 与远程 game_version 比较决定是否更新;取子 <game name="...">
* 作 app_data 的 app_gamename。`value` 为 version 标签独有;`name` 多标签共有,故按
* 所属标签(当前为子 game)区分抓取。
* 作 app_data 的 app_gamename;取子 <game id="..."> 作大厅 gameid 为空时的回退匹配 key。
*
* 实现说明:早期用 @kit.ArkTS xml.XmlPullParser,实测其 attributeValueCallbackFunction 对
* 本文件 `<version value="155">` 的 `value` 属性**不稳定触发**(同文件 `<game id>` 却能取到),
* 导致 version 恒为 0、子游戏每次都误判需更新而重复下载。改用确定性正则直接从原文抽属性,
* 语义等价于 Android `pullxml1`,规避解析器回调时序/属性触发的不确定性。
*/
import { xml, util } from '@kit.ArkTS';
import { Logger } from 'common';
/** version.xml 解析结果。 */
export interface VersionXmlInfo {
/** <version value="N"> 整数(缺失/失败 → 0)。 */
version: number;
@@ -20,46 +20,36 @@ export interface VersionXmlInfo {
}
export class VersionXml {
private static readonly log: Logger = Logger.tag('VersionXml');
// <version ... value="N" .../>:取版本号。允许属性间任意顺序/空白。
private static readonly VERSION_RE: RegExp = new RegExp('<version\\b[^>]*\\bvalue\\s*=\\s*"([^"]*)"', 'i');
// 子 <game <空白> ...>:根 <game> 无属性(其后紧跟 '>'),故 \s+ 自然跳过,只命中带属性的子 game。
private static readonly GAME_TAG_RE: RegExp = new RegExp('<game\\s+[^>]*?>', 'i');
private static readonly ID_RE: RegExp = new RegExp('\\bid\\s*=\\s*"([^"]*)"', 'i');
private static readonly NAME_RE: RegExp = new RegExp('\\bname\\s*=\\s*"([^"]*)"', 'i');
/** 解析 version + gameName。属性回调紧随其开始标签,故用 currentTag 把 name 归属到 game 标签。 */
/** 解析 version + gameName + gameId(任一缺失取默认值,绝不抛错)。 */
static parse(xmlText: string): VersionXmlInfo {
const info: VersionXmlInfo = { version: 0, gameName: '', gameId: '' };
if (xmlText.length === 0) {
return info;
}
let value: string = '';
let currentTag: string = '';
try {
const buf: Uint8Array = new util.TextEncoder().encodeInto(xmlText);
const parser: xml.XmlPullParser = new xml.XmlPullParser(buf.buffer as object as ArrayBuffer, 'UTF-8');
const options: xml.ParseOptions = {
ignoreNameSpace: true,
tagValueCallbackFunction: (name: string, _val: string): boolean => {
currentTag = name;
return true;
},
attributeValueCallbackFunction: (name: string, val: string): boolean => {
if (name === 'value') {
value = val;
} else if (name === 'name' && currentTag === 'game') {
// 根 <game> 无 name 属性;只有子 <game id name/> 触发,正是所需游戏名
info.gameName = val;
} else if (name === 'id' && currentTag === 'game') {
// 子 <game id="..."> 的 id:大厅 gameid 回退匹配 key(根 <game> 无 id 属性)
info.gameId = val;
}
return true;
},
};
parser.parseXml(options);
} catch (e) {
const err = e as Error;
VersionXml.log.w(`parse version.xml failed: ${err.message}`);
return info;
const verMatch: RegExpMatchArray | null = xmlText.match(VersionXml.VERSION_RE);
if (verMatch !== null) {
const n: number = parseInt(verMatch[1], 10);
info.version = isNaN(n) ? 0 : n;
}
const gameMatch: RegExpMatchArray | null = xmlText.match(VersionXml.GAME_TAG_RE);
if (gameMatch !== null) {
const tag: string = gameMatch[0];
const idMatch: RegExpMatchArray | null = tag.match(VersionXml.ID_RE);
if (idMatch !== null) {
info.gameId = idMatch[1];
}
const nameMatch: RegExpMatchArray | null = tag.match(VersionXml.NAME_RE);
if (nameMatch !== null) {
info.gameName = nameMatch[1];
}
}
const n: number = parseInt(value, 10);
info.version = isNaN(n) ? 0 : n;
return info;
}