From b1e3ce74331c8fdfb65e3095b6b12ff46e1413a1 Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Thu, 25 Jun 2026 19:43:11 +0800 Subject: [PATCH] =?UTF-8?q?M3(=E6=89=B93):=20=E5=BE=AE=E4=BF=A1=E5=88=86?= =?UTF-8?q?=E4=BA=AB=20+=20=E7=99=BB=E5=BD=95=EF=BC=88@tencent/wechat=5Fop?= =?UTF-8?q?en=5Fsdk=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依赖:ohpm i @tencent/wechat_open_sdk@1.0.19(feature_capabilities) T-M3-13 ShareProvider:friendsSharetypeUrlToptitleDescript → SendMessageToWXReq(scene 会话/朋友圈) + WXWebpageObject → 回传 sharesuccess{success(2成功/3取消),type(1好友/2朋友圈)}。 type 2截图/3图片/4视频暂回退网页分享(TODO);微信未安装回 cancel 不卡死 T-M3-14 LoginProvider:accreditlogin("1"=QQ空实现/其他=微信) → SendAuthReq(scope snsapi_userinfo) → 取 code。 🔴 AppSecret 不入端:code 换 profile 必须服务端,无端点时记录 code+TODO,不推不完整 sharelogin(红线) WeChatApi 单例:createWXAPI(AppID wxd2bd650e06bdfe58 公开标识) + sendReq + handleWant 路由 onResp EntryAbility.onCreate/onNewWant → WeChatApi.handleWant(接收微信回调) module.json5:querySchemes ["weixin"](isWXAppInstalled) 运行期成功前提(已注释说明):微信开放平台注册本 HarmonyOS 应用(bundleName+指纹绑 AppID)、登录 profile 需服务端 code 换取端点。 devecocli build 通过(CompileArkTS 编译微信代码无误)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- entry/oh-package-lock.json5 | 11 ++- entry/src/main/ets/di/AppModule.ets | 4 +- .../main/ets/entryability/EntryAbility.ets | 9 ++ entry/src/main/module.json5 | 3 + feature_capabilities/Index.ets | 5 + feature_capabilities/oh-package-lock.json5 | 8 ++ feature_capabilities/oh-package.json5 | 9 +- .../src/main/ets/providers/LoginProvider.ets | 68 ++++++++++++++ .../src/main/ets/providers/ShareProvider.ets | 75 +++++++++++++++ .../src/main/ets/wx/WeChatApi.ets | 91 +++++++++++++++++++ 10 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 feature_capabilities/src/main/ets/providers/LoginProvider.ets create mode 100644 feature_capabilities/src/main/ets/providers/ShareProvider.ets create mode 100644 feature_capabilities/src/main/ets/wx/WeChatApi.ets diff --git a/entry/oh-package-lock.json5 b/entry/oh-package-lock.json5 index d0dcb42..1593f06 100644 --- a/entry/oh-package-lock.json5 +++ b/entry/oh-package-lock.json5 @@ -6,6 +6,7 @@ "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { + "@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19", "common@../common": "common@../common", "contracts@../contracts": "contracts@../contracts", "domain_resource@../domain_resource": "domain_resource@../domain_resource", @@ -14,6 +15,13 @@ "platform@../platform": "platform@../platform" }, "packages": { + "@tencent/wechat_open_sdk@1.0.19": { + "name": "@tencent/wechat_open_sdk", + "version": "1.0.19", + "integrity": "sha512-elkkyMZVolWyhULd6zA+7xdHPRAhVja5ugyHvw/UpmeWrGfthHzNS6QtMZDMntzo7dFTNOxi8GAVvZmRA1uPBQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencent/wechat_open_sdk/-/wechat_open_sdk-1.0.19.har", + "registryType": "ohpm" + }, "common@../common": { "name": "common", "version": "1.0.0", @@ -60,7 +68,8 @@ "common": "file:../common", "platform": "file:../platform", "domain_resource": "file:../domain_resource", - "feature_bridge": "file:../feature_bridge" + "feature_bridge": "file:../feature_bridge", + "@tencent/wechat_open_sdk": "^1.0.19" } }, "platform@../platform": { diff --git a/entry/src/main/ets/di/AppModule.ets b/entry/src/main/ets/di/AppModule.ets index 81f7c18..a430013 100644 --- a/entry/src/main/ets/di/AppModule.ets +++ b/entry/src/main/ets/di/AppModule.ets @@ -7,7 +7,7 @@ */ import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider, VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider, - NetworkProvider } from 'feature_capabilities'; + NetworkProvider, ShareProvider, LoginProvider } from 'feature_capabilities'; export function buildCapabilities(): CapabilityProvider[] { return [ @@ -18,6 +18,8 @@ export function buildCapabilities(): CapabilityProvider[] { new ClipboardProvider(), // gameCopytext/gamepastetext new DeviceProvider(), // getTime/getphonestate/getbattery/getwifiLevel/getphoneInfo/getmarketname/getothername/getOther/getcompareCode new NetworkProvider(), // getnetwork + 出站广播 + new ShareProvider(), // friendsSharetypeUrlToptitleDescript → sharesuccess(微信) + new LoginProvider(), // accreditlogin → sharelogin(微信 code 模式,profile 待服务端) // —— 暂缓能力占位桩(§6.5)—— new RoomStubProvider(), new PayStubProvider(), diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 95070a5..b13220b 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -2,6 +2,7 @@ import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.Ab import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; import { EventBus } from 'common'; +import { WeChatApi } from 'feature_capabilities'; import { AppEvents } from '../routes/AppRoutes'; const DOMAIN = 0x0000; @@ -14,6 +15,14 @@ export default class EntryAbility extends UIAbility { hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err)); } hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + // 微信回调(冷启动经 want 拉回):交 SDK 解析 → 路由到 Share/Login 回调 + WeChatApi.getInstance().handleWant(want); + } + + onNewWant(want: Want, _launchParam: AbilityConstant.LaunchParam): void { + // 微信回调(应用已在后台时拉回) + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onNewWant'); + WeChatApi.getInstance().handleWant(want); } onDestroy(): void { diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index dd31218..9adba86 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -10,6 +10,9 @@ "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", + "querySchemes": [ + "weixin" + ], "abilities": [ { "name": "EntryAbility", diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index 8d7180b..fb80e25 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -14,3 +14,8 @@ export { ClipboardProvider } from './src/main/ets/providers/ClipboardProvider'; export { AppSystemProvider } from './src/main/ets/providers/AppSystemProvider'; export { DeviceProvider } from './src/main/ets/providers/DeviceProvider'; export { NetworkProvider } from './src/main/ets/providers/NetworkProvider'; +export { ShareProvider } from './src/main/ets/providers/ShareProvider'; +export { LoginProvider } from './src/main/ets/providers/LoginProvider'; + +// 微信 SDK 封装(EntryAbility 处理回调 want 需用) +export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi'; diff --git a/feature_capabilities/oh-package-lock.json5 b/feature_capabilities/oh-package-lock.json5 index 4876ab5..d3ee3dc 100644 --- a/feature_capabilities/oh-package-lock.json5 +++ b/feature_capabilities/oh-package-lock.json5 @@ -6,6 +6,7 @@ "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { + "@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19", "common@../common": "common@../common", "contracts@../contracts": "contracts@../contracts", "domain_resource@../domain_resource": "domain_resource@../domain_resource", @@ -13,6 +14,13 @@ "platform@../platform": "platform@../platform" }, "packages": { + "@tencent/wechat_open_sdk@1.0.19": { + "name": "@tencent/wechat_open_sdk", + "version": "1.0.19", + "integrity": "sha512-elkkyMZVolWyhULd6zA+7xdHPRAhVja5ugyHvw/UpmeWrGfthHzNS6QtMZDMntzo7dFTNOxi8GAVvZmRA1uPBQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencent/wechat_open_sdk/-/wechat_open_sdk-1.0.19.har", + "registryType": "ohpm" + }, "common@../common": { "name": "common", "version": "1.0.0", diff --git a/feature_capabilities/oh-package.json5 b/feature_capabilities/oh-package.json5 index 420f8d7..95a92af 100644 --- a/feature_capabilities/oh-package.json5 +++ b/feature_capabilities/oh-package.json5 @@ -10,6 +10,9 @@ "common": "file:../common", "platform": "file:../platform", "domain_resource": "file:../domain_resource", - "feature_bridge": "file:../feature_bridge" - } -} + "feature_bridge": "file:../feature_bridge", + "@tencent/wechat_open_sdk": "^1.0.19" + }, + "devDependencies": {}, + "dynamicDependencies": {} +} \ No newline at end of file diff --git a/feature_capabilities/src/main/ets/providers/LoginProvider.ets b/feature_capabilities/src/main/ets/providers/LoginProvider.ets new file mode 100644 index 0000000..789ea41 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/LoginProvider.ets @@ -0,0 +1,68 @@ +/** + * 登录能力(契约 §8.1/§10.2,T-M3-14)。微信开放 SDK,**code 模式**。 + * - accreditlogin:data "1"=QQ(契约即空实现)/ 其他=微信。 + * - 回传 sharelogin:用户资料 {openid/headimgurl/nickname/sex/city/province/unionid}。 + * + * 🔴 安全红线(§13/§14.2,CLAUDE.md 附录 B):AppSecret 绝不入客户端。 + * 客户端只取微信授权 code;用 code 换 access_token→userinfo→资料 **必须由服务端完成**, + * 服务端再把资料回给端、由本 Provider callHandler('sharelogin', 资料)。 + * 当前无服务端换取端点:记录 code 并留 TODO,不在端侧换 token,也不推不完整的 sharelogin。 + */ +import { SendAuthReq, ErrCode } from '@tencent/wechat_open_sdk'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; +import { WeChatApi } from '../wx/WeChatApi'; + +export class LoginProvider implements CapabilityProvider { + readonly name: string = 'login'; + private readonly log: Logger = Logger.tag('LoginProvider'); + private bridge: BridgeController | undefined = undefined; + private ctx: CapabilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.ctx = ctx; + bridge.registerHandler(InboundHandlers.AccreditLogin, (data: string, _cb: (resp: string) => void) => { + this.login(data); + }); + } + + private login(data: string): void { + if (data === '1') { + this.log.i('QQ login not implemented (contract: empty impl)'); + return; + } + const ctx = this.ctx; + if (ctx === undefined) { + return; + } + const wx: WeChatApi = WeChatApi.getInstance(); + if (!wx.isWXInstalled()) { + this.log.i('WeChat not installed; login aborted'); + return; + } + const req: SendAuthReq = new SendAuthReq(); + req.scope = 'snsapi_userinfo'; + req.state = 'tsgame'; + wx.sendAuth(ctx.uiAbilityContext, req, (resp) => { + if (resp.errCode === ErrCode.ERR_OK && resp.code !== undefined && resp.code !== '') { + this.exchangeCodeForProfile(resp.code); + } else { + this.log.i(`wx auth not granted: errCode=${resp.errCode}`); + } + }); + } + + /** + * 用 code 换取用户资料(必须经服务端)。当前无服务端端点:记录 code、留 TODO。 + * 接入后:POST code → 服务端返回 ShareLoginResp → bridge.callHandler('sharelogin', JSON)。 + */ + private exchangeCodeForProfile(code: string): void { + this.log.i(`got wx auth code (len=${code.length}); server exchange required, not pushing sharelogin yet`); + // TODO(M3 服务端就绪后):POST code 到服务端换取 ShareLoginResp(contracts), + // 再 this.bridge?.callHandler(OutboundHandlers.ShareLogin, JSON.stringify(profile))。 + // AppSecret 仅在服务端,端侧只传 code。 + } +} diff --git a/feature_capabilities/src/main/ets/providers/ShareProvider.ets b/feature_capabilities/src/main/ets/providers/ShareProvider.ets new file mode 100644 index 0000000..964771d --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/ShareProvider.ets @@ -0,0 +1,75 @@ +/** + * 分享能力(契约 §8.1/§10.1,T-M3-13)。微信开放 SDK。 + * - friendsSharetypeUrlToptitleDescript:data=sharetypeBean JSON。 + * sharefriend "2"→朋友圈 / 其他→会话;type "1"网页/"2"Canvas截图/"3"图片/"4"视频。 + * - 回传 sharesuccess {success(2成功/3取消), type(1好友/2朋友圈)}。 + * + * 本版完整支持 type=1(网页分享);type 2/3/4 暂回退为网页分享(webpageUrl),见 TODO。 + * ⚠ 运行期成功需在微信开放平台注册本 HarmonyOS 应用(bundleName+指纹 绑定 AppID)。 + */ +import { SendMessageToWXReq, WXMediaMessage, WXWebpageObject, ErrCode } from '@tencent/wechat_open_sdk'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; +import { WeChatApi } from '../wx/WeChatApi'; + +export class ShareProvider implements CapabilityProvider { + readonly name: string = 'share'; + private readonly log: Logger = Logger.tag('ShareProvider'); + private bridge: BridgeController | undefined = undefined; + private ctx: CapabilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.ctx = ctx; + bridge.registerHandler(InboundHandlers.FriendsShare, (data: string, _cb: (resp: string) => void) => { + this.share(data); + }); + } + + private share(data: string): void { + const ctx = this.ctx; + if (ctx === undefined) { + return; + } + let bean: SharetypeBean; + try { + bean = JSON.parse(data) as SharetypeBean; + } catch (e) { + this.log.w(`share bad json: ${(e as Error).message}`); + return; + } + const typeNum: number = bean.sharefriend === '2' ? 2 : 1; + + const wx: WeChatApi = WeChatApi.getInstance(); + if (!wx.isWXInstalled()) { + this.log.i('WeChat not installed; report cancel'); + this.reportResult(3, typeNum); + return; + } + + const req: SendMessageToWXReq = new SendMessageToWXReq(); + req.scene = bean.sharefriend === '2' ? SendMessageToWXReq.WXSceneTimeline : SendMessageToWXReq.WXSceneSession; + + const msg: WXMediaMessage = new WXMediaMessage(); + msg.title = bean.title; + msg.description = bean.description; + // TODO(M3):type "2" Canvas 截图(需容器 runJavaScript canvas.toDataURL + LocalUploadServer)、 + // "3" 图片链接(下载后 WXImageObject)、"4" 视频。当前统一按网页分享回退,保证可分享不报错。 + const web: WXWebpageObject = new WXWebpageObject(); + web.webpageUrl = bean.webpageUrl; + msg.mediaObject = web; + req.message = msg; + + wx.sendShare(ctx.uiAbilityContext, req, (resp) => { + const success: number = resp.errCode === ErrCode.ERR_OK ? 2 : 3; + this.reportResult(success, typeNum); + }); + } + + private reportResult(success: number, type: number): void { + const out: ShareSuccessResp = { success, type }; + this.bridge?.callHandler(OutboundHandlers.ShareSuccess, JSON.stringify(out)); + } +} diff --git a/feature_capabilities/src/main/ets/wx/WeChatApi.ets b/feature_capabilities/src/main/ets/wx/WeChatApi.ets new file mode 100644 index 0000000..1113e7f --- /dev/null +++ b/feature_capabilities/src/main/ets/wx/WeChatApi.ets @@ -0,0 +1,91 @@ +/** + * 微信开放平台 SDK 封装(@tencent/wechat_open_sdk,契约 §10.1/§10.2/§13)。单例。 + * + * 职责:持有 WXApi(用 AppID 创建)、发起授权/分享请求、接收微信回调并路由到对应回调。 + * AppID 为公开标识,可入客户端;**AppSecret 绝不入端**(登录换 profile 须服务端,§13 红线)。 + * + * 回调路径:微信处理完拉回宿主 App(默认 EntryAbility),EntryAbility.onNewWant/onCreate + * 把 want 交给 handleWant() → SDK 解析 → onResp 路由到 authCb/shareCb。 + */ +import { common, Want } from '@kit.AbilityKit'; +import { + WXAPIFactory, WXApi, WXApiEventHandler, BaseReq, BaseResp, + SendAuthResp, SendMessageToWXResp, +} from '@tencent/wechat_open_sdk'; +import { Logger } from 'common'; + +/** 微信 AppID(公开标识,契约 §13)。 */ +export const WX_APP_ID: string = 'wxd2bd650e06bdfe58'; + +export type AuthRespCallback = (resp: SendAuthResp) => void; +export type ShareRespCallback = (resp: SendMessageToWXResp) => void; + +export class WeChatApi { + private static readonly log: Logger = Logger.tag('WeChatApi'); + private static inst: WeChatApi | undefined = undefined; + private readonly api: WXApi; + private readonly handler: WXApiEventHandler; + private authCb: AuthRespCallback | undefined = undefined; + private shareCb: ShareRespCallback | undefined = undefined; + + private constructor() { + this.api = WXAPIFactory.createWXAPI(WX_APP_ID); + this.handler = { + onReq: (_req: BaseReq) => { }, + onResp: (resp: BaseResp) => this.routeResp(resp), + }; + } + + static getInstance(): WeChatApi { + if (WeChatApi.inst === undefined) { + WeChatApi.inst = new WeChatApi(); + } + return WeChatApi.inst; + } + + isWXInstalled(): boolean { + try { + return this.api.isWXAppInstalled(); + } catch (e) { + WeChatApi.log.w(`isWXAppInstalled failed: ${(e as Error).message}`); + return false; + } + } + + /** 发起授权登录;resp 经 cb 异步回调。 */ + sendAuth(context: common.UIAbilityContext, req: BaseReq, cb: AuthRespCallback): void { + this.authCb = cb; + this.api.sendReq(context, req); + } + + /** 发起分享;resp 经 cb 异步回调。 */ + sendShare(context: common.UIAbilityContext, req: BaseReq, cb: ShareRespCallback): void { + this.shareCb = cb; + this.api.sendReq(context, req); + } + + /** 由 EntryAbility 在 onNewWant/onCreate 调用,处理微信回调 want。 */ + handleWant(want: Want): void { + try { + this.api.handleWant(want, this.handler); + } catch (e) { + WeChatApi.log.w(`handleWant failed: ${(e as Error).message}`); + } + } + + private routeResp(resp: BaseResp): void { + if (resp instanceof SendAuthResp) { + const cb = this.authCb; + this.authCb = undefined; + if (cb !== undefined) { + cb(resp); + } + } else if (resp instanceof SendMessageToWXResp) { + const cb = this.shareCb; + this.shareCb = undefined; + if (cb !== undefined) { + cb(resp); + } + } + } +}