diff --git a/entry/src/main/ets/di/AppModule.ets b/entry/src/main/ets/di/AppModule.ets new file mode 100644 index 0000000..e1cebb5 --- /dev/null +++ b/entry/src/main/ets/di/AppModule.ets @@ -0,0 +1,20 @@ +/** + * 组装根(框架 §6.2 DI)。**唯一知道"全部能力"的地方**。 + * + * 桥核心、容器都不感知具体能力;新增/裁剪能力只改这里一行。 + * M2:仅示例能力(验证插件模式)。M3:替换为全部真实 Provider + 占位桩 + * (RoomStubProvider/PayStubProvider,§6.5),契约 handler 名/数量不变。 + */ +import { CapabilityProvider, EchoSampleProvider } from 'feature_capabilities'; + +export function buildCapabilities(): CapabilityProvider[] { + return [ + new EchoSampleProvider(), + // —— M3 在此装配(顺序无关,互不依赖)—— + // new ShareProvider(), new LoginProvider(), new LocationProvider(), new AudioProvider(), + // new ShakeProvider(), new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(), + // new VibrateProvider(), new ScanProvider(), new CameraProvider(), new PhotoProvider(), + // new NavProvider(), new AppSystemProvider(), + // new RoomStubProvider(), new PayStubProvider(), // 暂缓能力占位桩(§6.5) + ]; +} diff --git a/entry/src/main/ets/pages/BridgeGameContainer.ets b/entry/src/main/ets/pages/BridgeGameContainer.ets index 34c7940..6a4ee2a 100644 --- a/entry/src/main/ets/pages/BridgeGameContainer.ets +++ b/entry/src/main/ets/pages/BridgeGameContainer.ets @@ -1,13 +1,19 @@ import { webview } from '@kit.ArkWeb'; +import { common } from '@kit.AbilityKit'; import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge'; -import { AppEnv } from 'common'; -import { BridgeGameParams } from '../routes/AppRoutes'; +import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities'; +import { ConfigManager } from 'domain_resource'; +import { KvStore, LocalUploadServer } from 'platform'; +import { OutboundHandlers } from 'contracts'; +import { AppEnv, EventBus, Logger } from 'common'; +import { BridgeGameParams, AppEvents } from '../routes/AppRoutes'; +import { buildCapabilities } from '../di/AppModule'; /** * 大厅/子游戏容器(对应 webviewActivity,框架 §7.1)。 - * M1:挂 Web + 接桥(onControllerAttached 建桥 / onLoadIntercept 接桥 / onPageEnd 注桥), - * 加载最小回显 H5 验证 JsBridge 协议与 V1(yy:// 拦截)。 - * M2:入口 URL 改为大厅 file://.../gamehall/index.html;M3:注册全部能力 Provider。 + * 挂 Web + 接桥 + 经组装根注册全部能力 + 首次进度 100% 推 appservice/setPostUrl。 + * 入口 URL 由 StartupOrchestrator 经 params.entryUrl 传入(file://.../index.html?Launchtype=0); + * 为空时回退最小回显页(V1 验证用)。 */ @Component export struct BridgeGameContainer { @@ -16,46 +22,76 @@ export struct BridgeGameContainer { private controller: webview.WebviewController = new webview.WebviewController(); private adapter: WebviewControllerAdapter | undefined = undefined; private bridge: BridgeController | undefined = undefined; + private registrar: CapabilityRegistrar | undefined = undefined; + private uploadServer: LocalUploadServer = new LocalUploadServer(); + private firstProgressDone: boolean = false; + private cancelForeground: (() => void) | undefined = undefined; + private cancelBackground: (() => void) | undefined = undefined; aboutToAppear(): void { // 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线) if (AppEnv.isDebug()) { webview.WebviewController.setWebDebuggingAccess(true); } + // 前后台联动:EntryAbility 经 EventBus 广播 → 转给各能力 + callHandler('appservice') + this.cancelForeground = EventBus.on(AppEvents.FOREGROUND, () => { + this.registrar?.forEachForeground(); + this.bridge?.callHandler(OutboundHandlers.AppService, '1'); + }); + this.cancelBackground = EventBus.on(AppEvents.BACKGROUND, () => { + this.registrar?.forEachBackground(); + this.bridge?.callHandler(OutboundHandlers.AppService, '2'); + }); } aboutToDisappear(): void { - // 释放:先清桥(回执表/注册表/启动队列),再断 UI 线程通道,最后置空防残留命中 - const b = this.bridge; - if (b !== undefined) { - b.dispose(); + // 释放顺序:能力 onDestroy → 清桥(回执表/注册表/启动队列)→ 断 UI 通道 → 退订事件 → 置空 + this.registrar?.destroyAll(); + this.bridge?.dispose(); + this.adapter?.dispose(); + if (this.cancelForeground !== undefined) { + this.cancelForeground(); } - const a = this.adapter; - if (a !== undefined) { - a.dispose(); + if (this.cancelBackground !== undefined) { + this.cancelBackground(); } + this.uploadServer.stop(); + this.registrar = undefined; this.bridge = undefined; this.adapter = undefined; } + /** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1:能力注册放这里)。 */ private setupBridge(): void { + const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; const adapter = new WebviewControllerAdapter(this.controller); const bridge = new BridgeController(adapter); - bridge.setBridgeJs(BridgeJsLoader.load(getContext(this))); - // 注册最小回显所需的原生 handler(M3 替换为全部能力 Provider) - bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => { - cb(Date.now().toString()); - }); - bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => { - cb(data); - }); + 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; + this.uploadServer.start(); + } + + /** 入口 URL:params 非空用之,否则回退最小回显页(V1)。 */ + private webSrc(): string | Resource { + return this.params.entryUrl !== '' ? this.params.entryUrl : $rawfile('test_echo.html'); } build() { NavDestination() { - Web({ src: $rawfile('test_echo.html'), controller: this.controller }) + Web({ src: this.webSrc(), controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .fileAccess(true) @@ -74,9 +110,17 @@ export struct BridgeGameContainer { return b !== undefined ? b.onLoadIntercept(url) : false; }) .onPageEnd(() => { - const b = this.bridge; - if (b !== undefined) { - b.onPageEnd(); + this.bridge?.onPageEnd(); + }) + .onProgressChange((event: OnProgressChangeEvent) => { + // 首次进度 100% → 推 appservice(前台) + setPostUrl(与 Android onProgressChanged==100 一致) + if (event.newProgress === 100 && !this.firstProgressDone) { + this.firstProgressDone = true; + const b = this.bridge; + if (b !== undefined) { + b.callHandler(OutboundHandlers.AppService, '1'); + b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl()); + } } }) } diff --git a/entry/src/main/ets/pages/SplashPage.ets b/entry/src/main/ets/pages/SplashPage.ets index ac36808..0237b38 100644 --- a/entry/src/main/ets/pages/SplashPage.ets +++ b/entry/src/main/ets/pages/SplashPage.ets @@ -1,29 +1,62 @@ +import { common } from '@kit.AbilityKit'; +import { StartupOrchestrator, StartupResult, StartupStage } from 'domain_resource'; import { RouteName, BridgeGameParams } from '../routes/AppRoutes'; /** - * 启动引导页壳(对应 weclomeactivity1)。 - * M0:仅路由壳,点击进入大厅容器。M2:接入 StartupOrchestrator(配置/资源/app_data 注入)后再跳。 + * 启动引导页(对应 weclomeactivity1,框架 §4/§8.1)。 + * 进入即跑 StartupOrchestrator(本地配置→远程配置→资源准备→app_data 注入), + * 完成后 replace 进大厅容器;被 showmessage 阻断时弹公告。 */ @Component export struct SplashPage { pathStack: NavPathStack = new NavPathStack(); + @State private stageText: string = '正在启动…'; + @State private percent: number = 0; + @State private blocked: string = ''; - private enterHall(): void { - const params: BridgeGameParams = { entryUrl: '', launchType: '0' }; - this.pathStack.replacePathByName(RouteName.BRIDGE_GAME, params, false); + aboutToAppear(): void { + this.runStartup(); + } + + private async runStartup(): Promise { + const ctx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; + const orchestrator = new StartupOrchestrator(ctx, (stage: StartupStage, percent: number) => { + this.stageText = stage; + this.percent = percent; + }); + try { + const result: StartupResult = await orchestrator.run(); + if (result.enter) { + const params: BridgeGameParams = { entryUrl: result.entryUrl, launchType: '0' }; + this.pathStack.replacePathByName(RouteName.BRIDGE_GAME, params, false); + } else { + this.blocked = result.message; + } + } catch (e) { + const err = e as Error; + this.blocked = `启动失败:${err.message}`; + } } build() { NavDestination() { Column({ space: 16 }) { - Text('TSGame 启动引导') - .fontSize(22) + Text('TSGame') + .fontSize(28) .fontWeight(FontWeight.Bold) - Text('M0 脚手架 · 路由壳') - .fontSize(14) - .fontColor(Color.Gray) - Button('进入大厅') - .onClick(() => this.enterHall()) + if (this.blocked === '') { + Text(`${this.stageText} ${this.percent}%`) + .fontSize(14) + .fontColor(Color.Gray) + Progress({ value: this.percent, total: 100, type: ProgressType.Linear }) + .width('60%') + } else { + Text(this.blocked) + .fontSize(16) + .fontColor(Color.Red) + .textAlign(TextAlign.Center) + .padding({ left: 24, right: 24 }) + } } .width('100%') .height('100%') diff --git a/entry/src/main/resources/rawfile/gamehall_builtin.zip b/entry/src/main/resources/rawfile/gamehall_builtin.zip new file mode 100644 index 0000000..f7fb503 Binary files /dev/null and b/entry/src/main/resources/rawfile/gamehall_builtin.zip differ diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index f66291a..f27dd6e 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -1,3 +1,8 @@ -// feature_capabilities HAR —— 能力层(导出入口 SSOT) -// 各子模块实现完成后在此统一 export。占位常量确保 HAR 可编译。 -export const feature_capabilities_MODULE_VERSION: string = '1.0.0'; +// feature_capabilities HAR —— 能力层(导出入口 SSOT,框架 §6) + +// 插件框架 +export { CapabilityProvider, CapabilityContext } from './src/main/ets/core/CapabilityProvider'; +export { CapabilityRegistrar } from './src/main/ets/core/CapabilityRegistrar'; + +// 示例能力(M2 脚手架,M3 替换为真实 Provider) +export { EchoSampleProvider } from './src/main/ets/providers/EchoSampleProvider'; diff --git a/feature_capabilities/src/main/ets/core/CapabilityProvider.ets b/feature_capabilities/src/main/ets/core/CapabilityProvider.ets new file mode 100644 index 0000000..4ad8857 --- /dev/null +++ b/feature_capabilities/src/main/ets/core/CapabilityProvider.ets @@ -0,0 +1,40 @@ +/** + * 能力插件契约(框架 §6.1)。**桥核心对能力零感知**:能力以插件形式在容器初始化时 + * 把自己的入站 handler 注册进桥,并持有 bridge 以便异步推送出站 handler。 + * + * 新增/裁剪一个原生能力 = 新增/删除一个 CapabilityProvider + 在组装根 buildCapabilities() 增删一行, + * 零侵入桥核心(杜绝桥层 import 任何具体能力)。 + */ +import { common } from '@kit.AbilityKit'; +import { BridgeController } from 'feature_bridge'; +import { ConfigManager } from 'domain_resource'; +import { KvStore, LocalUploadServer } from 'platform'; +import { Logger } from 'common'; + +/** 能力运行上下文(注入给每个 Provider)。 */ +export interface CapabilityContext { + /** UIAbility 上下文(拉起系统能力/弹窗/权限)。 */ + uiAbilityContext: common.UIAbilityContext; + /** 配置(取 agent/market/other 等暴露给 H5 的值)。 */ + config: ConfigManager; + /** 键值存储。 */ + kv: KvStore; + /** 本机上传端点(截图分享用,M3)。 */ + uploadServer: LocalUploadServer; + /** 通用日志。 */ + log: Logger; +} + +/** 能力插件接口。 */ +export interface CapabilityProvider { + /** 能力名(如 'location'/'share'),用于日志与诊断。 */ + readonly name: string; + /** 把本能力的入站 handler 注册到桥;持有 bridge 以便异步推送。 */ + register(bridge: BridgeController, ctx: CapabilityContext): void; + /** 应用切前台(可选,联动 §A.1)。 */ + onForeground?(): void; + /** 应用切后台(可选)。 */ + onBackground?(): void; + /** 容器销毁清理(可选,解绑监听/释放资源)。 */ + onDestroy?(): void; +} diff --git a/feature_capabilities/src/main/ets/core/CapabilityRegistrar.ets b/feature_capabilities/src/main/ets/core/CapabilityRegistrar.ets new file mode 100644 index 0000000..b1b062a --- /dev/null +++ b/feature_capabilities/src/main/ets/core/CapabilityRegistrar.ets @@ -0,0 +1,50 @@ +/** + * 能力注册器(框架 §6.2)。把一组 Provider 批量注册到桥,并统一转发生命周期。 + * + * 容器持有一个 Registrar:onControllerAttached 时 register(),前后台时 onForeground/onBackground(), + * aboutToDisappear 时 onDestroy()。桥核心始终不感知任何具体能力。 + */ +import { BridgeController } from 'feature_bridge'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from './CapabilityProvider'; + +export class CapabilityRegistrar { + private static readonly log: Logger = Logger.tag('CapabilityRegistrar'); + private readonly providers: CapabilityProvider[]; + + constructor(providers: CapabilityProvider[]) { + this.providers = providers; + } + + /** 注册全部 Provider 的入站 handler。 */ + registerAll(bridge: BridgeController, ctx: CapabilityContext): void { + for (const p of this.providers) { + p.register(bridge, ctx); + CapabilityRegistrar.log.i(`registered capability: ${p.name}`); + } + } + + forEachForeground(): void { + for (const p of this.providers) { + if (p.onForeground !== undefined) { + p.onForeground(); + } + } + } + + forEachBackground(): void { + for (const p of this.providers) { + if (p.onBackground !== undefined) { + p.onBackground(); + } + } + } + + destroyAll(): void { + for (const p of this.providers) { + if (p.onDestroy !== undefined) { + p.onDestroy(); + } + } + } +} diff --git a/feature_capabilities/src/main/ets/providers/EchoSampleProvider.ets b/feature_capabilities/src/main/ets/providers/EchoSampleProvider.ets new file mode 100644 index 0000000..7053736 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/EchoSampleProvider.ets @@ -0,0 +1,23 @@ +/** + * 示例能力(M2 脚手架)。验证 CapabilityProvider 插件模式可注册并被桥分发, + * 对接最小回显 H5(test_echo.html 用 getTime/nativeEcho)。 + * + * ⚠ M3 用真实 Provider(DeviceProvider 等)替换: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)'); + } +}