42e1c2e34b
Swift 侧(协议与 Android UploadPlugin.kt 逐字一致,编译/真机留 G3): - UploadBridge.swift:通道/方法/事件/失败码常量 + registerTask 参数解析 (镜像 UploadProtocol.kt) - UploadEventBus.swift:单调 seq + 终态 ack 缓存重放(镜像 UploadEventBus.kt),iOS 增强——未 ack 终态落盘(系统后台拉起投递的 回调在进程重启后同样重放,技术方案 §6.2 持久化原生回调) - UploadTaskStore.swift:taskId→URLSession taskIdentifier 映射 plist 持久化 (Application Support;对应 Android WorkManager 自持映射) - UploadPlugin.swift:URLSession background (com.photolive.photo_live.uploads)+ delegate 失败分类(2xx→completed+etag / 403→needsRenew / 4xx→HTTP_4XX / 网络→重试上限后 NETWORK,镜像 UploadPutExecutor/UploadWorker)、原生重试(5 次上限、10s 指数退避)、 进度 500ms 节流、queryTask/cancelTask/ackEvents、静默取消 - AppDelegate.swift:implicit engine 通道注册 + 启动重建会话 + handleEventsForBackgroundURLSession completionHandler 暂存 - Info.plist:UIBackgroundModes 有意不声明(background URLSession 无需; 计划措辞按技术方案 §6.2 修正,见 README-ios.md) - project.pbxproj 登记新 Swift 文件;README-ios.md(Xcode 步骤 + G3 验证 清单:实际文件上传/持久化回调窗口/服务端补偿/重启重联校准/强制退出/ 低电量延迟/100 张批量) Dart:NativeTransport 选择矩阵并入 iOS(Platform.isAndroid || isIOS, FORCE_DART_TRANSPORT 覆盖不变)——协议两侧一致,无新通道代码。 flutter analyze 0 issue;flutter test 219 全绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
108 lines
3.9 KiB
Swift
108 lines
3.9 KiB
Swift
import Foundation
|
||
|
||
/// 上传桥接协议常量与 registerTask 参数解析(P3 计划 Task 8)。
|
||
///
|
||
/// 与 Android 侧 `UploadProtocol.kt` 逐字对齐:通道名、方法名、事件名、
|
||
/// 失败码完全一致;Dart 侧 `lib/features/uploads/native_transport.dart`
|
||
/// 的 [UploadBridgeChannels] 为同一协议的单源镜像。
|
||
enum UploadBridge {
|
||
/// 命令通道(registerTask / cancelTask / queryTask / ackEvents)。
|
||
static let methodChannel = "photolive/uploads"
|
||
|
||
/// 事件流(progress / completed / needsRenew / failed,带单调 seq)。
|
||
static let eventChannel = "photolive/uploads/events"
|
||
|
||
static let methodRegister = "registerTask"
|
||
static let methodCancel = "cancelTask"
|
||
static let methodQuery = "queryTask"
|
||
static let methodAck = "ackEvents"
|
||
|
||
static let eventProgress = "progress"
|
||
static let eventCompleted = "completed"
|
||
static let eventNeedsRenew = "needsRenew"
|
||
static let eventFailed = "failed"
|
||
|
||
/// 网络不可达/超时(重试上限耗尽后上报)。
|
||
static let failNetwork = "NETWORK"
|
||
|
||
/// 4xx 客户端错误(status 随事件携带;403 单独走 needsRenew)。
|
||
static let failHttp4xx = "HTTP_4XX"
|
||
|
||
/// 5xx 服务端错误(重试上限耗尽后上报)。
|
||
static let failServerError = "SERVER_ERROR"
|
||
|
||
/// 源文件缺失(受管副本被清理,不可恢复)。
|
||
static let failSourceMissing = "SOURCE_MISSING"
|
||
|
||
/// 后台 URLSession 标识(App 重启后系统按此重联会话)。
|
||
static let sessionIdentifier = "com.photolive.photo_live.uploads"
|
||
}
|
||
|
||
/// registerTask 参数(Dart → 原生),字段与 Kotlin `UploadRegisterSpec` 一致。
|
||
struct UploadRegisterSpec {
|
||
let taskId: String
|
||
let url: String
|
||
let method: String
|
||
let headers: [String: String]
|
||
let filePath: String
|
||
let uploadId: String?
|
||
let attempt: Int
|
||
}
|
||
|
||
/// 参数形状不符(插件层转 bad_args 错误,镜像 Kotlin IllegalArgumentException)。
|
||
struct UploadBridgeBadArgsError: Error {
|
||
let message: String
|
||
}
|
||
|
||
/// registerTask 参数解析(纯函数;与 Kotlin `parseRegisterSpec` 同语义)。
|
||
///
|
||
/// 通道解码所得字典为 [String: Any](Flutter 标准编解码 null → NSNull),
|
||
/// 显式 null 与类型不符同样归为 bad_args。
|
||
func parseRegisterSpec(_ args: [String: Any]) throws -> UploadRegisterSpec {
|
||
func str(_ key: String) throws -> String {
|
||
guard let v = args[key] as? String else {
|
||
throw UploadBridgeBadArgsError(
|
||
message: args[key] == nil ? "missing '\(key)'" : "'\(key)' must be a String"
|
||
)
|
||
}
|
||
return v
|
||
}
|
||
let taskId = try str("taskId")
|
||
let url = try str("url")
|
||
let methodRaw = try str("method")
|
||
let method = methodRaw.isEmpty ? "PUT" : methodRaw
|
||
let filePath = try str("filePath")
|
||
var headers: [String: String] = [:]
|
||
if let raw = args["headers"] as? [String: Any] {
|
||
for (k, v) in raw {
|
||
guard let value = v as? String else {
|
||
throw UploadBridgeBadArgsError(message: "'headers' must be Map<String, String>")
|
||
}
|
||
headers[k] = value
|
||
}
|
||
} else if args["headers"] != nil {
|
||
throw UploadBridgeBadArgsError(message: "'headers' must be a Map")
|
||
}
|
||
var uploadId: String?
|
||
var attempt = 0
|
||
if let metadata = args["metadata"] as? [String: Any] {
|
||
uploadId = metadata["uploadId"] as? String
|
||
if let v = metadata["attempt"] as? NSNumber {
|
||
attempt = v.intValue
|
||
} else if let v = metadata["attempt"] as? Int {
|
||
attempt = v
|
||
} else if metadata["attempt"] != nil {
|
||
throw UploadBridgeBadArgsError(message: "'metadata.attempt' must be an int")
|
||
}
|
||
}
|
||
return UploadRegisterSpec(
|
||
taskId: taskId,
|
||
url: url,
|
||
method: method,
|
||
headers: headers,
|
||
filePath: filePath,
|
||
uploadId: uploadId,
|
||
attempt: attempt
|
||
)
|
||
}
|