Phase 4.C:QQShare URL Scheme 真实落地(不依赖 SDK)

移植 msext QQShareManager.m:716-771 simpleShareToQQFriend 简化版到 Swift。

QQShare.share 完整实现:
  - canOpenURL("mqqapi://") 检测 QQ 安装;未装返回 .notInstalled
  - 仅链接分享走完整 URL Scheme(type == "1");type == "2"/其它(截图 /
    远端图)暂返回 .success 避免 H5 业务卡住,Phase 4.F 接入
  - URL 构造(与 msext 等价):
    * scene == .timeline → mqqapi://share/to_qzone? (QQ 空间)
    * scene == .friend   → mqqapi://share/to_fri?   (QQ 好友)
    * 参数:version=1 + cflag=0 + req_type=1(无 url 时 req_type=0)+
      url=<encoded> + title=<encoded> + description=<encoded>
  - encode 函数与 msext QQShareManager.m:1262-1268 等价:仅保留 RFC 3986
    unreserved 字符(alphanumeric + "-._~"),其它全部 percent encode
  - canOpenURL 二次检查 + await UIApplication.open
  - 调起即视为 success(msext 同款乐观策略,URL Scheme 单向无返回)

Info.plist 新增 LSApplicationQueriesSchemes(iOS 9+ canOpenURL 白名单
要求,否则总返回 false):
  - mqq / mqqapi / mqqopensdkfriend / mqqopensdkapiV2/V3/V4(QQ 相关)
  - snssdk1128(抖音,Phase 4.D 用)
  - 微信相关待 Phase 4.E 接入 SDK 时再加(weixin / weixinULAPI 等)

BuildProject 通过

Plan §5 Phase 4.3 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 20:04:36 +08:00
co-authored by Claude Opus 4.7
parent 2ad4563d16
commit aff5ee7633
3 changed files with 88 additions and 15 deletions
+6 -3
View File
@@ -486,10 +486,13 @@ Contract Design Plan(本文档)
- `Source/Share/{Wechat,QQ,Douyin}Share.swift`:三个平台 stub(仅返回 success),Phase 4.C/4.D/4.E 升级 - `Source/Share/{Wechat,QQ,Douyin}Share.swift`:三个平台 stub(仅返回 success),Phase 4.C/4.D/4.E 升级
- `FriendsShareHandler` 升级:调 ShareCenter.dispatch + 完成后触发 sharesuccess 反向 callback(success/cancelled 都发,notInstalled/failed 不发,沿用 msext 乐观策略) - `FriendsShareHandler` 升级:调 ShareCenter.dispatch + 完成后触发 sharesuccess 反向 callback(success/cancelled 都发,notInstalled/failed 不发,沿用 msext 乐观策略)
- BuildProject 通过 - BuildProject 通过
- [ ] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK** - [x] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK**(4.C 完成)
- 移植 msext `QQShareManager.m:716-771` `simpleShareToQQFriend` 简化版 - 移植 msext `QQShareManager.m:716-771` `simpleShareToQQFriend` 简化版
- URL 构造:`mqqapi://share/to_fri?version=1&cflag=0&req_type=1&url=...&title=...&description=...` - URL 构造:`mqqapi://share/to_fri?` 或 `mqqapi://share/to_qzone?`(scene == .timeline 走 qzone)+ `version=1&cflag=0&req_type=1&url=...&title=...&description=...`
- `canOpenURL` 检查 + `openURL` 异步打开 - `encode` 与 msext line 1262-1268 等价:仅保留 RFC 3986 unreserved(alphanumeric + `-._~`)其它 percent encode
- `canOpenURL` 检查 + `await UIApplication.open` 异步打开;调起即视为 success
- **当前仅链接分享**(type == "1");type == "2" 截图 / 远端图 留 Phase 4.F
- Info.plist 加 `LSApplicationQueriesSchemes`:`mqq` / `mqqapi` / `mqqopensdkfriend` / `mqqopensdkapiV2/V3/V4`
- [ ] **4.4** **抖音分享走 URL Scheme + Photos,不依赖 SDK** - [ ] **4.4** **抖音分享走 URL Scheme + Photos,不依赖 SDK**
- 移植 msext `DouyinShareManager.m` 简化版 - 移植 msext `DouyinShareManager.m` 简化版
- URL Scheme:`snssdk1128://share/video` / `snssdk1128://camera` - URL Scheme:`snssdk1128://share/video` / `snssdk1128://camera`
+17
View File
@@ -47,5 +47,22 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<!--
LSApplicationQueriesSchemes — canOpenURL 白名单(iOS 9+ 必须声明,否则
canOpenURL 总返回 false)。本项目分享接入:
- QQ(URL Scheme 路径,不依赖 SDK):mqq / mqqapi / mqqopensdkfriend 等
- 抖音(URL Scheme + Photos 路径,不依赖 SDK):snssdk1128
- 微信(Phase 4.E SDK 接入时再加 weixin / weixinULAPI / weixinURLParamsAPI)
-->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mqq</string>
<string>mqqapi</string>
<string>mqqopensdkfriend</string>
<string>mqqopensdkapiV2</string>
<string>mqqopensdkapiV3</string>
<string>mqqopensdkapiV4</string>
<string>snssdk1128</string>
</array>
</dict> </dict>
</plist> </plist>
+65 -12
View File
@@ -2,23 +2,35 @@
// QQShare.swift // QQShare.swift
// ylgamehall // ylgamehall
// //
// QQ 分享 SharePlatform 实现 stub(Phase 4.C 真实落地)。 // QQ 分享 SharePlatform 实现(Phase 4.C 完整)。
// //
// Phase 4.B 阶段为 stub(仅返回 success,让 SharePanel 流程跑通); // 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend 的 URL Scheme
// Phase 4.C 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend // fallback。**不依赖 QQ SDK**。
// 的 URL Scheme fallback(mqqapi://share/to_fri?...)。 //
// URL 构造:
// mqqapi://share/to_fri?version=1&cflag=0&req_type=1
// &url=<encoded webpageUrl>&title=<encoded title>&description=<encoded desc>
//
// encodeString 与 msext line 1262-1268 等价:unreserved 字符集(RFC 3986
// alphanumeric + "-._~"),其它全部 percent encode。
//
// ⚠️ 当前仅支持链接分享(type == "1")。type == "2" 截图 / 其它 远端图
// 在 Phase 4.F 接入(依赖 UIGraphicsImageRenderer + Photos / 临时文件 +
// mqqapi://share/to_fri?file_type=img 路径)。
// //
import UIKit import UIKit
import os.log
@MainActor @MainActor
public final class QQShare: SharePlatform { public final class QQShare: SharePlatform {
public static let shared = QQShare() public static let shared = QQShare()
private static let log = Logger(subsystem: "ylgamehall", category: "QQShare")
public let name = "QQ" public let name = "QQ"
/// 检测 QQ app 是否安装:canOpenURL("mqqapi://")
public var isInstalled: Bool { public var isInstalled: Bool {
guard let url = URL(string: "mqqapi://") else { return false } guard let url = URL(string: "mqqapi://") else { return false }
return UIApplication.shared.canOpenURL(url) return UIApplication.shared.canOpenURL(url)
@@ -28,12 +40,53 @@ public final class QQShare: SharePlatform {
public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult {
guard isInstalled else { return .notInstalled(platform: name) } guard isInstalled else { return .notInstalled(platform: name) }
// Phase 4.B 阶段 stub:返回 success,Phase 4.C 升级为完整 URL Scheme 调起
// 真实实现(Phase 4.C): // 仅链接分享走完整 URL Scheme;图片分享留 Phase 4.F
// - 拼装 mqqapi://share/to_fri?version=1&cflag=0&req_type=1 guard content.type == "1" else {
// &url=...&title=...&description=... Self.log.warning("QQShare: type=\(content.type, privacy: .public) (非链接) 暂未实现,返回 success 避免 H5 业务卡住")
// - canOpenURL + open return .success
// - 调起即视为 success(URL Scheme 单向无返回,msext 同款乐观策略) }
return .success
// 拼装 URL:mqqapi://share/to_fri? 或 mqqapi://share/to_qzone?
// scene == .timeline 走 QQ 空间 to_qzone;.friend 走 to_fri(QQ 好友)
let host = scene == .timeline ? "share/to_qzone" : "share/to_fri"
var parts: [String] = [
"version=1",
"cflag=0",
"req_type=1"
]
if !content.webpageUrl.isEmpty {
parts.append("url=\(Self.encode(content.webpageUrl))")
} else {
// 无 URL → 改 req_type=0 文本分享
parts = ["version=1", "cflag=0", "req_type=0"]
}
if !content.title.isEmpty {
parts.append("title=\(Self.encode(content.title))")
}
if !content.desc.isEmpty {
parts.append("description=\(Self.encode(content.desc))")
}
let urlString = "mqqapi://\(host)?\(parts.joined(separator: "&"))"
Self.log.debug("QQShare open: \(urlString, privacy: .public)")
guard let qqURL = URL(string: urlString),
UIApplication.shared.canOpenURL(qqURL)
else {
return .failed(reason: "invalid QQ URL")
}
// URL Scheme 单向调起:msext 同款乐观策略 — 调起即视为 success
let opened = await UIApplication.shared.open(qqURL)
return opened ? .success : .failed(reason: "openURL failed")
}
/// 与 msext QQShareManager.m:1262-1268 等价:仅保留 RFC 3986 unreserved
/// 字符(alphanumeric + "-._~"),其它全部 percent encode。
nonisolated static func encode(_ string: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-._~")
return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
} }
} }