From 1d231a52efcb86f78371483c9a937efc3957ef67 Mon Sep 17 00:00:00 2001 From: joywayer Date: Wed, 24 Jun 2026 18:45:05 +0800 Subject: [PATCH] =?UTF-8?q?4.B=20QQ=20=E9=93=BE=E6=8E=A5=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E6=94=B9=20previewimagedata=20=E5=86=85=E8=81=94=20base64?= =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E7=BC=A9=E7=95=A5=E5=9B=BE=E7=A0=B4=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测控制台 `unable to make sandbox extension: [1: Operation not permitted]` 证实 previewimageUrl=file://... 跨 app 沙盒无效 —— QQ 进程读不到调用方 app tmp 目录里的文件,渲染破图占位。 msext 这段实现一直是装饰性代码(线上同样破图但没人改)。新外壳按原则 B 现代化修正: - 字段名改用 QQ Open SDK 实际约定的 `previewimagedata`(值=图像字节 base64,而非 file path base64) - 内容来源改用 app icon JPEG 0.6 编码后 base64 内联到 URL - 删 writeAppIconToTemp(不再写临时文件) App icon 60-180px JPEG 0.6 后 base64 约 5-15KB,iOS openURL ~64KB 限额下 余量充足。这条修正违反"原则 A 等价 msext"吗?不——契约 §3.1[2] sharesuccess 反向 callback 已经在 commit 9bd5a81 落地,与 H5 唯一可见 的契约面(callback 时机与载荷)保持等价。msext 这处缩略图字段错写属于 原项目内部 bug,新外壳的"原则 B 内部自由重构"覆盖范围,按现代标准修正 不破契约。 --- ylgamehall/Source/Share/QQShare.swift | 35 ++++++++++++--------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/ylgamehall/Source/Share/QQShare.swift b/ylgamehall/Source/Share/QQShare.swift index f141ee0..5373dff 100644 --- a/ylgamehall/Source/Share/QQShare.swift +++ b/ylgamehall/Source/Share/QQShare.swift @@ -114,11 +114,15 @@ public final class QQShare: SharePlatform { parts.append("url=\(Self.encode(Self.base64(content.webpageUrl)))") } - // 3. 缩略图 —— app icon 写到 tmp 目录 → previewimageUrl = urlenc(base64("file://")) - // 对齐 msext QQShareManager.m:517-522 saveImageToTempDirectory + base64 - if let thumbPath = Self.writeAppIconToTemp() { - let fileURL = "file://\(thumbPath)" - parts.append("previewimageUrl=\(Self.encode(Self.base64(fileURL)))") + // 3. 缩略图 —— 字段名是 previewimagedata(图像字节 base64),不是 + // msext 写的 previewimageUrl=base64("file://..."): 后者跨 app 沙盒读不到 + // 本地文件(控制台 `unable to make sandbox extension` 即此原因),实测 + // QQ 渲染破图占位。改对齐 QQ Open SDK 的 previewimagedata 字段,把 + // app icon JPEG 字节本身 base64 内联到 URL。 + // iOS openURL URL 长度限制 ~64KB,app icon 60-180px JPEG 0.6 编码后 + // base64 ≈ 5-15KB,余量充足。 + if let thumbBase64 = Self.appIconBase64() { + parts.append("previewimagedata=\(Self.encode(thumbBase64))") } let urlString = "mqqapi://\(host)?\(parts.joined(separator: "&"))" @@ -191,21 +195,14 @@ public final class QQShare: SharePlatform { return top } - // MARK: - 缩略图(app icon → tmp file) + // MARK: - 缩略图(app icon JPEG → base64 内联到 URL) - /// 把 app icon JPEG 写到 NSTemporaryDirectory,返回绝对路径。 - /// 对齐 msext QQShareManager.m:1271-1278 saveImageToTempDirectory: - /// 文件名 qq_share_.jpg,JPEG 0.7 质量。失败返回 nil。 - private static func writeAppIconToTemp() -> String? { - guard let data = appIconImage().jpegData(compressionQuality: 0.7) else { return nil } - let path = NSTemporaryDirectory() + "qq_share_\(UUID().uuidString).jpg" - do { - try data.write(to: URL(fileURLWithPath: path), options: .atomic) - return path - } catch { - log.error("QQShare 写缩略图临时文件失败 \(error.localizedDescription, privacy: .public)") - return nil - } + /// app icon JPEG 0.6 → base64 字符串。QQ Open SDK previewimagedata 字段的值。 + /// 跨 app 沙盒下 file:// 路径不可读(控制台 `sandbox extension`),唯一可 + /// 靠的传递方式是把图像字节直接 base64 编码到 URL 参数里。 + private static func appIconBase64() -> String? { + guard let data = appIconImage().jpegData(compressionQuality: 0.6) else { return nil } + return data.base64EncodedString() } /// app icon UIImage,与 WechatShare.appIconImage() 同源策略