M3(T-M2-04/T-M3-13): LocalUploadServer 截图上传分享链路落地

补「H5 主动 POST 截图上传分享」链路,对齐原工程 OkHttpPhotoServer + savehandler:
- LocalUploadServer 从骨架实现为真实本机 HTTP server(@ohos.net.socket TCPSocketServer):
  127.0.0.1:8099 起逐端口重试 listen;手解极简 HTTP/1.1(\r\n\r\n 分隔 + Content-Length
  累积读全 body);仅 POST /testurl,body 取 base64(form name=/raw 兼容)+type → emit
  PHOTO_UPLOAD → 回 200;非 /testurl 回 404;16MB 上限;异常 log 不崩溃。
  baseUrl 反映实际端口。注:TCPSocketServer 无显式 close,stop 用 off('connect')+释放引用
- common ShareEvents.PHOTO_UPLOAD + PhotoUploadPayload
- ShareProvider:订阅 PHOTO_UPLOAD(仅激活槽处理,避免双实例重复分享)→ 落盘 → 微信图片
  分享(type=="1"好友/其他朋友圈)→ sharesuccess;onDestroy 退订;抽 wechatImageByFriend/
  wechatSceneByFriend 供面板截图与 POST 上传两条链路共用

真机验证项:H5 真实 POST body 格式、127.0.0.1 端口可达、微信开放平台注册

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-26 08:19:21 +08:00
co-authored by Claude Opus 4.8
parent cb3673cb1b
commit 187e73a549
4 changed files with 352 additions and 19 deletions
@@ -24,7 +24,7 @@ import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
import { BridgeController } from 'feature_bridge';
import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts';
import { EventBus, Logger, ShareEvents, SharePanelRequest, SharePanelResult } from 'common';
import { EventBus, Logger, ShareEvents, SharePanelRequest, SharePanelResult, PhotoUploadPayload } from 'common';
import { FileSystem } from 'platform';
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
import { WeChatApi } from '../wx/WeChatApi';
@@ -39,6 +39,8 @@ export class ShareProvider implements CapabilityProvider {
private ctx: CapabilityContext | undefined = undefined;
/** 截图落盘 + resultEvent 命名计数器(实例自增,避免 Math.random 串扰)。 */
private seq: number = 0;
/** PHOTO_UPLOAD 订阅退订函数(onDestroy 调用,防双槽实例泄漏/串扰)。 */
private photoUploadCancel: (() => void) | undefined = undefined;
constructor() {
ShareProvider.instanceCount += 1;
@@ -51,6 +53,55 @@ export class ShareProvider implements CapabilityProvider {
bridge.registerHandler(InboundHandlers.FriendsShare, (data: string, _cb: (resp: string) => void) => {
this.onShare(data);
});
// 第二条链路:H5 主动 POST 截图到 LocalUploadServer → emit PHOTO_UPLOAD → 微信图片分享。
this.photoUploadCancel = EventBus.on(ShareEvents.PHOTO_UPLOAD, (p) => this.onPhotoUpload(p));
}
/** 容器销毁:退订 PHOTO_UPLOAD(双槽各一实例,须 per-subscriber 退订,勿 offAll)。 */
onDestroy(): void {
if (this.photoUploadCancel !== undefined) {
this.photoUploadCancel();
this.photoUploadCancel = undefined;
}
}
/**
* H5 POST 截图上传分享:仅当本槽桥激活时处理(非激活槽忽略,避免双实例对同一上传重复分享)。
* type 语义同 sharefriend"1" 好友 → WXSceneSession/回传 type=1;其他 → 朋友圈/回传 type=2。
* 落盘成功走微信图片分享;微信未装 reportResult(3, ...);落盘失败仅 log(无网页可降级)。
*/
private onPhotoUpload(payload?: Object): void {
const ctx = this.ctx;
const bridge = this.bridge;
if (ctx === undefined || bridge === undefined) {
return;
}
if (!bridge.isActive()) {
return; // 非激活槽不处理,保证同一上传仅激活槽分享一次
}
if (payload === undefined) {
return;
}
const up = payload as PhotoUploadPayload;
if (up.imageBase64 === '') {
this.log.w('photo upload empty image');
return;
}
const friendType: number = up.type === '1' ? 1 : 2;
const wx: WeChatApi = WeChatApi.getInstance();
if (!wx.isWXInstalled()) {
this.log.i('WeChat not installed (photo upload); report cancel');
this.reportResult(3, friendType);
return;
}
// saveDataUrlToFile 对无 data: 前缀的纯 base64 也兼容(indexOf(',')<0 → 整串当 base64
const filePath: string = this.saveDataUrlToFile(ctx.uiAbilityContext, up.imageBase64);
if (filePath === '') {
this.log.w('photo upload save failed');
this.reportResult(3, friendType);
return;
}
this.wechatImageByFriend(ctx.uiAbilityContext, wx, filePath, friendType);
}
/** 入站:校验 bean → 发起面板请求(一对一回投)。 */
@@ -134,7 +185,12 @@ export class ShareProvider implements CapabilityProvider {
}
private wechatScene(bean: SharetypeBean): number {
return bean.sharefriend === '1' ? SendMessageToWXReq.WXSceneSession : SendMessageToWXReq.WXSceneTimeline;
return this.wechatSceneByFriend(bean.sharefriend === '1' ? 1 : 2);
}
/** friendType: 1 好友(WXSceneSession) / 2 朋友圈(WXSceneTimeline)。 */
private wechatSceneByFriend(friendType: number): number {
return friendType === 1 ? SendMessageToWXReq.WXSceneSession : SendMessageToWXReq.WXSceneTimeline;
}
private wechatWebPage(ctx: common.UIAbilityContext, wx: WeChatApi, bean: SharetypeBean, reportType: number): void {
@@ -154,15 +210,21 @@ export class ShareProvider implements CapabilityProvider {
private wechatImage(ctx: common.UIAbilityContext, wx: WeChatApi, filePath: string,
bean: SharetypeBean, reportType: number): void {
this.wechatImageByFriend(ctx, wx, filePath, reportType);
}
/** 微信图片分享(友/圈由 friendType 决定);分享面板与 H5 POST 上传两条链路共用。
* friendType 同时作为 sharesuccess 回传的 type1 好友 / 2 朋友圈)。 */
private wechatImageByFriend(ctx: common.UIAbilityContext, wx: WeChatApi, filePath: string, friendType: number): void {
const req: SendMessageToWXReq = new SendMessageToWXReq();
req.scene = this.wechatScene(bean);
req.scene = this.wechatSceneByFriend(friendType);
const msg: WXMediaMessage = new WXMediaMessage();
const img: WXImageObject = new WXImageObject();
img.uri = fileUri.getUriFromPath(filePath); // 沙箱文件 file uriSDK 支持 jpeg/png
msg.mediaObject = img;
req.message = msg;
wx.sendShare(ctx, req, (resp) => {
this.reportResult(resp.errCode === ErrCode.ERR_OK ? 2 : 3, reportType);
this.reportResult(resp.errCode === ErrCode.ERR_OK ? 2 : 3, friendType);
});
}