M3 补全:移除 EchoSample 样板 + getcompareCode 接版本决策 + app_gamename

- 移除 EchoSampleProvider(组装根/导出/文件):M3 全部 Provider 已就位,
  getTime 由 DeviceProvider 提供;样板回显仅 M1 测试页用,生产无依赖
- getcompareCode:启动期 StartupOrchestrator 把网络 App 版本(VersionDecision.appVersion)
  存 KvStore(KV_KEY_NET_APP_VERSION),DeviceProvider 同步读出与本地 config.appversion 比较,
  本地>网络→'1' 否则'0'(对齐 Android apputil.code 语义)
- app_gamename:VersionXml.parse 解析子 <game name>;ResourceManager.localGameName 暴露;
  AppDataInjector.buildValues 接 gameName 参数;大厅/子游戏两处注入均填入

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-26 07:33:13 +08:00
co-authored by Claude Opus 4.8
parent cc5db07ddd
commit 01e4c0e99e
10 changed files with 72 additions and 53 deletions
+2 -2
View File
@@ -10,10 +10,10 @@ export { RemoteConfig, AgentNode, ChannelNode, MarketNode, GameOverrideNode, Gam
export { VersionResolver } from './src/main/ets/version/VersionResolver'; export { VersionResolver } from './src/main/ets/version/VersionResolver';
// 资源 // 资源
export { VersionXml } from './src/main/ets/resource/VersionXml'; export { VersionXml, VersionXmlInfo } from './src/main/ets/resource/VersionXml';
export { ResourceManager, ResourcePaths } from './src/main/ets/resource/ResourceManager'; export { ResourceManager, ResourcePaths } from './src/main/ets/resource/ResourceManager';
export { AppDataInjector, AppDataValues } from './src/main/ets/resource/AppDataInjector'; export { AppDataInjector, AppDataValues } from './src/main/ets/resource/AppDataInjector';
// 启动编排 // 启动编排
export { StartupOrchestrator, StartupStage, StartupResult, StageCallback } export { StartupOrchestrator, StartupStage, StartupResult, StageCallback, KV_KEY_NET_APP_VERSION }
from './src/main/ets/startup/StartupOrchestrator'; from './src/main/ets/startup/StartupOrchestrator';
@@ -42,8 +42,8 @@ export interface AppDataValues {
export class AppDataInjector { export class AppDataInjector {
private static readonly log: Logger = Logger.tag('AppDataInjector'); private static readonly log: Logger = Logger.tag('AppDataInjector');
/** 由本地配置 + 资源版本 + 启动类型构建 app_data 值(大厅 launchtype='0' / 子游戏 '1')。 */ /** 由本地配置 + 资源版本 + 启动类型 + 游戏名构建 app_data 值(大厅 launchtype='0' / 子游戏 '1')。 */
static buildValues(config: AppConfig, version: string, launchtype: string): AppDataValues { static buildValues(config: AppConfig, version: string, launchtype: string, gameName: string = ''): AppDataValues {
return { return {
app_version: version, app_version: version,
app_gameconfig: config.gameconfig, app_gameconfig: config.gameconfig,
@@ -55,8 +55,8 @@ export class AppDataInjector {
app_channel: config.channel, app_channel: config.channel,
app_invitationcode: config.tuiguang, app_invitationcode: config.tuiguang,
app_Launchtype: launchtype, app_Launchtype: launchtype,
// TODO(M3)app_gamename 源为 version.xml 的 <game name>,当前 VersionXml 仅解析 version。 // app_gamename 源为 version.xml 的 <game name>ResourceManager.localGameName 提供)
app_gamename: '', app_gamename: gameName,
app_getwifisignalLevel: '0', app_getwifisignalLevel: '0',
}; };
} }
@@ -13,7 +13,7 @@ import { common } from '@kit.AbilityKit';
import { Downloader, FileSystem, KvStore, ProgressCallback, Unzipper } from 'platform'; import { Downloader, FileSystem, KvStore, ProgressCallback, Unzipper } from 'platform';
import { Logger } from 'common'; import { Logger } from 'common';
import { AppConfig } from '../config/AppConfig'; import { AppConfig } from '../config/AppConfig';
import { VersionXml } from './VersionXml'; import { VersionXml, VersionXmlInfo } from './VersionXml';
/** 资源路径集合。 */ /** 资源路径集合。 */
export interface ResourcePaths { export interface ResourcePaths {
@@ -73,18 +73,28 @@ export class ResourceManager {
return FileSystem.exists(this.cachedPaths.indexPath); return FileSystem.exists(this.cachedPaths.indexPath);
} }
/** 本地资源版本(version.xml 的 version;缺失返回 0)。 */ /** 解析本地 version.xml(取首个存在的候选;缺失 → 默认空信息)。 */
localVersion(): number { private readVersionXml(): VersionXmlInfo {
const candidates: string[] = [ const candidates: string[] = [
`${this.cachedPaths.gameDir}/version.xml`, `${this.cachedPaths.gameDir}/version.xml`,
`${this.cachedPaths.urlpath}/version.xml`, `${this.cachedPaths.urlpath}/version.xml`,
]; ];
for (const p of candidates) { for (const p of candidates) {
if (FileSystem.exists(p)) { if (FileSystem.exists(p)) {
return VersionXml.parseVersion(FileSystem.readText(p)); return VersionXml.parse(FileSystem.readText(p));
} }
} }
return 0; return { version: 0, gameName: '' };
}
/** 本地资源版本(version.xml 的 version;缺失返回 0)。 */
localVersion(): number {
return this.readVersionXml().version;
}
/** 本地游戏名(version.xml 子 <game name>;缺失返回 ''),写入 app_data 的 app_gamename。 */
localGameName(): string {
return this.readVersionXml().gameName;
} }
/** 持久化路径到 KvStore(供容器/重启读取)。 */ /** 持久化路径到 KvStore(供容器/重启读取)。 */
@@ -2,29 +2,47 @@
* version.xml 解析(契约 §5.3,框架 §8.3)。用 @kit.ArkTS xml.XmlPullParser。 * version.xml 解析(契约 §5.3,框架 §8.3)。用 @kit.ArkTS xml.XmlPullParser。
* *
* 结构:<game><agent id name/><game id name/><channel id name/><version value name/></game> * 结构:<game><agent id name/><game id name/><channel id name/><version value name/></game>
* 用途:取 <version value="42"> 与远程 game_version 比较决定是否更新 * 用途:取 <version value="42"> 与远程 game_version 比较决定是否更新;取子 <game name="...">
* `value` 属性为 version 标签独有,故用 attributeValueCallbackFunction 即可稳定抓取。 * 作 app_data 的 app_gamename。`value` 为 version 标签独有`name` 多标签共有,故按
* 所属标签(当前为子 game)区分抓取。
*/ */
import { xml, util } from '@kit.ArkTS'; import { xml, util } from '@kit.ArkTS';
import { Logger } from 'common'; import { Logger } from 'common';
/** version.xml 解析结果。 */
export interface VersionXmlInfo {
/** <version value="N"> 整数(缺失/失败 → 0)。 */
version: number;
/** 子 <game name="..."> 游戏名(缺失 → '')。 */
gameName: string;
}
export class VersionXml { export class VersionXml {
private static readonly log: Logger = Logger.tag('VersionXml'); private static readonly log: Logger = Logger.tag('VersionXml');
/** 解析 version 整数(解析失败/缺失 → 0。 */ /** 解析 version + gameName。属性回调紧随其开始标签,故用 currentTag 把 name 归属到 game 标签。 */
static parseVersion(xmlText: string): number { static parse(xmlText: string): VersionXmlInfo {
const info: VersionXmlInfo = { version: 0, gameName: '' };
if (xmlText.length === 0) { if (xmlText.length === 0) {
return 0; return info;
} }
let value: string = ''; let value: string = '';
let currentTag: string = '';
try { try {
const buf: Uint8Array = new util.TextEncoder().encodeInto(xmlText); const buf: Uint8Array = new util.TextEncoder().encodeInto(xmlText);
const parser: xml.XmlPullParser = new xml.XmlPullParser(buf.buffer as object as ArrayBuffer, 'UTF-8'); const parser: xml.XmlPullParser = new xml.XmlPullParser(buf.buffer as object as ArrayBuffer, 'UTF-8');
const options: xml.ParseOptions = { const options: xml.ParseOptions = {
ignoreNameSpace: true, ignoreNameSpace: true,
tagValueCallbackFunction: (name: string, _val: string): boolean => {
currentTag = name;
return true;
},
attributeValueCallbackFunction: (name: string, val: string): boolean => { attributeValueCallbackFunction: (name: string, val: string): boolean => {
if (name === 'value') { if (name === 'value') {
value = val; value = val;
} else if (name === 'name' && currentTag === 'game') {
// 根 <game> 无 name 属性;只有子 <game id name/> 触发,正是所需游戏名
info.gameName = val;
} }
return true; return true;
}, },
@@ -33,9 +51,15 @@ export class VersionXml {
} catch (e) { } catch (e) {
const err = e as Error; const err = e as Error;
VersionXml.log.w(`parse version.xml failed: ${err.message}`); VersionXml.log.w(`parse version.xml failed: ${err.message}`);
return 0; return info;
} }
const n: number = parseInt(value, 10); const n: number = parseInt(value, 10);
return isNaN(n) ? 0 : n; info.version = isNaN(n) ? 0 : n;
return info;
}
/** 解析 version 整数(解析失败/缺失 → 0)。 */
static parseVersion(xmlText: string): number {
return VersionXml.parse(xmlText).version;
} }
} }
@@ -16,6 +16,9 @@ import { VersionDecision } from '../config/RemoteConfig';
import { ResourceManager } from '../resource/ResourceManager'; import { ResourceManager } from '../resource/ResourceManager';
import { AppDataInjector, AppDataValues } from '../resource/AppDataInjector'; import { AppDataInjector, AppDataValues } from '../resource/AppDataInjector';
/** KvStore 键:启动期算出的网络 App 版本(供 DeviceProvider getcompareCode 同步比较)。 */
export const KV_KEY_NET_APP_VERSION: string = 'net_app_version';
/** 启动阶段(用于进度展示/可观测)。 */ /** 启动阶段(用于进度展示/可观测)。 */
export enum StartupStage { export enum StartupStage {
INIT = 'INIT', INIT = 'INIT',
@@ -122,6 +125,10 @@ export class StartupOrchestrator {
StartupOrchestrator.log.w(`blocked by showmessage: ${decision.showmessage}`); StartupOrchestrator.log.w(`blocked by showmessage: ${decision.showmessage}`);
return { enter: false, entryUrl: '', message: decision.showmessage }; return { enter: false, entryUrl: '', message: decision.showmessage };
} }
// 缓存网络 App 版本,供 DeviceProvider getcompareCode 同步比较(离线 decision 缺失则保留上次值)
if (decision !== undefined) {
kv.putNumber(KV_KEY_NET_APP_VERSION, decision.appVersion);
}
// 5) 版本更新(依赖内置资源已就绪 + 远程决策) // 5) 版本更新(依赖内置资源已就绪 + 远程决策)
const localVer: number = resource.localVersion(); const localVer: number = resource.localVersion();
@@ -144,7 +151,7 @@ export class StartupOrchestrator {
// 6) 注入 app_data.js(务必在进大厅之前)。大厅 launchtype='0' // 6) 注入 app_data.js(务必在进大厅之前)。大厅 launchtype='0'
this.stage(StartupStage.INJECT_APP_DATA, 92); this.stage(StartupStage.INJECT_APP_DATA, 92);
const values: AppDataValues = AppDataInjector.buildValues(local, `${resource.localVersion()}`, '0'); const values: AppDataValues = AppDataInjector.buildValues(local, `${resource.localVersion()}`, '0', resource.localGameName());
AppDataInjector.inject(resource.paths().gameDir, values); AppDataInjector.inject(resource.paths().gameDir, values);
// 7) 进大厅 // 7) 进大厅
+1 -4
View File
@@ -5,14 +5,13 @@
* M2:仅示例能力(验证插件模式)。M3:替换为全部真实 Provider + 占位桩 * M2:仅示例能力(验证插件模式)。M3:替换为全部真实 Provider + 占位桩
* RoomStubProvider/PayStubProvider,§6.5),契约 handler 名/数量不变。 * RoomStubProvider/PayStubProvider,§6.5),契约 handler 名/数量不变。
*/ */
import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider, import { CapabilityProvider, RoomStubProvider, PayStubProvider,
VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider, VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider,
NetworkProvider, ShareProvider, LoginProvider, PhotoProvider, ShakeProvider, NetworkProvider, ShareProvider, LoginProvider, PhotoProvider, ShakeProvider,
ScanProvider, CameraProvider, LocationProvider, AudioProvider, NavProvider } from 'feature_capabilities'; ScanProvider, CameraProvider, LocationProvider, AudioProvider, NavProvider } from 'feature_capabilities';
export function buildCapabilities(): CapabilityProvider[] { export function buildCapabilities(): CapabilityProvider[] {
return [ return [
new EchoSampleProvider(), // M2 回显样板(M3 全部 Provider 就位后可移除)
// —— M3 已落地能力(顺序无关,互不依赖)—— // —— M3 已落地能力(顺序无关,互不依赖)——
new AppSystemProvider(), // orientation/browser/finsh/openApplyDownloadpath/notification new AppSystemProvider(), // orientation/browser/finsh/openApplyDownloadpath/notification
new VibrateProvider(), // vibrator/repeatvibrator/canclevibrator new VibrateProvider(), // vibrator/repeatvibrator/canclevibrator
@@ -31,7 +30,5 @@ export function buildCapabilities(): CapabilityProvider[] {
// —— 暂缓能力占位桩(§6.5)—— // —— 暂缓能力占位桩(§6.5)——
new RoomStubProvider(), new RoomStubProvider(),
new PayStubProvider(), new PayStubProvider(),
// —— M3 待落地:DeviceProvider/NetworkProvider/ShakeProvider/LocationProvider/AudioProvider/
// ScanProvider/CameraProvider/PhotoProvider/NavProvider/ShareProvider/LoginProvider ——
]; ];
} }
@@ -133,7 +133,7 @@ export struct BridgeGameContainer {
} }
const subDir: string = `${res.paths().urlpath}/${d.dir}`; const subDir: string = `${res.paths().urlpath}/${d.dir}`;
// 子游戏目录自带 app_data.jsH5 同步读),launchtype='1' // 子游戏目录自带 app_data.jsH5 同步读),launchtype='1'
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1'); const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1', res.localGameName());
AppDataInjector.inject(subDir, values); AppDataInjector.inject(subDir, values);
this.slotL?.deactivate(); this.slotL?.deactivate();
this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer); this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer);
-3
View File
@@ -4,9 +4,6 @@
export { CapabilityProvider, CapabilityContext } from './src/main/ets/core/CapabilityProvider'; export { CapabilityProvider, CapabilityContext } from './src/main/ets/core/CapabilityProvider';
export { CapabilityRegistrar } from './src/main/ets/core/CapabilityRegistrar'; export { CapabilityRegistrar } from './src/main/ets/core/CapabilityRegistrar';
// 示例能力(M2 脚手架,M3 逐步替换为真实 Provider
export { EchoSampleProvider } from './src/main/ets/providers/EchoSampleProvider';
// —— M3 能力 Provider —— // —— M3 能力 Provider ——
export { RoomStubProvider, PayStubProvider } from './src/main/ets/providers/StubProviders'; export { RoomStubProvider, PayStubProvider } from './src/main/ets/providers/StubProviders';
export { VibrateProvider } from './src/main/ets/providers/VibrateProvider'; export { VibrateProvider } from './src/main/ets/providers/VibrateProvider';
@@ -13,7 +13,8 @@ import { wifiManager } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { BridgeController } from 'feature_bridge'; import { BridgeController } from 'feature_bridge';
import { InboundHandlers, OutboundHandlers, PhoneInfoBean, WifiLevelResp } from 'contracts'; import { InboundHandlers, OutboundHandlers, PhoneInfoBean, WifiLevelResp } from 'contracts';
import { ConfigManager, AppConfig } from 'domain_resource'; import { ConfigManager, AppConfig, KV_KEY_NET_APP_VERSION } from 'domain_resource';
import { KvStore } from 'platform';
import { Logger } from 'common'; import { Logger } from 'common';
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
@@ -22,10 +23,12 @@ export class DeviceProvider implements CapabilityProvider {
private readonly log: Logger = Logger.tag('DeviceProvider'); private readonly log: Logger = Logger.tag('DeviceProvider');
private bridge: BridgeController | undefined = undefined; private bridge: BridgeController | undefined = undefined;
private config: ConfigManager | undefined = undefined; private config: ConfigManager | undefined = undefined;
private kv: KvStore | undefined = undefined;
register(bridge: BridgeController, ctx: CapabilityContext): void { register(bridge: BridgeController, ctx: CapabilityContext): void {
this.bridge = bridge; this.bridge = bridge;
this.config = ctx.config; this.config = ctx.config;
this.kv = ctx.kv;
bridge.registerHandler(InboundHandlers.GetTime, (_d: string, cb: (resp: string) => void) => { bridge.registerHandler(InboundHandlers.GetTime, (_d: string, cb: (resp: string) => void) => {
cb(Date.now().toString()); cb(Date.now().toString());
@@ -43,8 +46,12 @@ export class DeviceProvider implements CapabilityProvider {
cb(this.lookupConfig(data)); cb(this.lookupConfig(data));
}); });
bridge.registerHandler(InboundHandlers.GetCompareCode, (_d: string, cb: (resp: string) => void) => { bridge.registerHandler(InboundHandlers.GetCompareCode, (_d: string, cb: (resp: string) => void) => {
// TODO(M3)1=本地 appversion>网络 app_version/0=否,需接 StartupOrchestrator 的版本决策;暂安全返回 '0' // 契约:本地 App 版本 > 网络版本 → '1',否则 '0'(对齐 Android apputil.code)。
cb('0'); // 网络版本由 StartupOrchestrator 启动期算出存入 KvStore;本地版本为 config.appversion。
const localVer: number = parseInt(this.local().appversion, 10);
const local: number = isNaN(localVer) ? 0 : localVer;
const net: number = this.kv?.getNumber(KV_KEY_NET_APP_VERSION, 0) ?? 0;
cb(local > net ? '1' : '0');
}); });
bridge.registerHandler(InboundHandlers.GetBattery, (_d: string, _cb: (resp: string) => void) => { bridge.registerHandler(InboundHandlers.GetBattery, (_d: string, _cb: (resp: string) => void) => {
this.pushBattery(); this.pushBattery();
@@ -1,23 +0,0 @@
/**
* 示例能力(M2 脚手架)。验证 CapabilityProvider 插件模式可注册并被桥分发,
* 对接最小回显 H5test_echo.html 用 getTime/nativeEcho)。
*
* ⚠ M3 用真实 ProviderDeviceProvider 等)替换:buildCapabilities() 删除本行即可,
* 桥核心与容器零改动。
*/
import { BridgeController } from 'feature_bridge';
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
export class EchoSampleProvider implements CapabilityProvider {
readonly name: string = 'echo(sample)';
register(bridge: BridgeController, ctx: CapabilityContext): void {
bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => {
cb(Date.now().toString());
});
bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => {
cb(data);
});
ctx.log.i('echo sample registered (getTime/nativeEcho)');
}
}