feat(update): 添加 API Key 管理和 WebSocket 实时通知功能

- 新增 API Key 管理功能,支持外部工具认证调用平台 API
- 实现 WebSocket 实时通知,版本发布时推送轻量通知给客户端
- 添加 APK 文件哈希校验,支持已下载检测和直接安装
- 支持外部 APK 上传使用 API Key 认证
- 优化私有化部署自动注入 nginx WebSocket 代理配置
- 扩展 SDK 功能包括已下载检测、直接安装和实时通知监听
这个提交包含在:
XuqmGroup 2026-06-11 12:25:16 +08:00
父节点 5b34c60838
当前提交 bfebd8a99a
共有 3 个文件被更改,包括 73 次插入9 次删除

查看文件

@ -30,10 +30,32 @@ public extension SDKConfig {
} }
} }
extension SDKConfig {
}
public enum SDKEndpoints { public enum SDKEndpoints {
public static let apiBaseURL = URL(string: "https://dev.xuqinmin.com")! private static var _apiBaseURL: URL?
public static let imWebSocketURL = URL(string: "wss://dev.xuqinmin.com/ws/im")! private static var _imWebSocketURL: URL?
public static var apiBaseURL: URL {
_apiBaseURL ?? URL(string: "https://dev.xuqinmin.com")!
}
public static var imWebSocketURL: URL {
_imWebSocketURL ?? URL(string: "wss://dev.xuqinmin.com/ws/im")!
}
public static func configure(
apiBaseURL: String? = nil,
imWebSocketURL: String? = nil
) {
if let api = apiBaseURL, let url = URL(string: api) {
_apiBaseURL = url
}
if let ws = imWebSocketURL, let url = URL(string: ws) {
_imWebSocketURL = url
}
}
public static func reset() {
_apiBaseURL = nil
_imWebSocketURL = nil
}
} }

查看文件

@ -1,20 +1,56 @@
import Foundation import Foundation
import Security
public final class TokenStore: @unchecked Sendable { public final class TokenStore: @unchecked Sendable {
private let key = "com.xuqm.sdk.token" private let service = "com.xuqm.sdk.token"
private let account = "auth_token"
public init() {} public init() {}
public func save(_ token: String) { public func save(_ token: String) {
UserDefaults.standard.set(token, forKey: key) delete()
guard let data = token.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
]
SecItemAdd(query as CFDictionary, nil)
} }
public func get() -> String? { public func get() -> String? {
UserDefaults.standard.string(forKey: key) let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
} }
public func clear() { public func clear() {
UserDefaults.standard.removeObject(forKey: key) delete()
}
private func delete() {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
SecItemDelete(query as CFDictionary)
} }
} }

查看文件

@ -75,6 +75,12 @@ public final class XuqmSDK: NSObject {
if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId { if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId {
throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId) throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId)
} }
if let baseUrl = file.baseUrl {
SDKEndpoints.configure(apiBaseURL: baseUrl)
}
if let serverUrl = file.serverUrl {
SDKEndpoints.configure(imWebSocketURL: serverUrl + "/ws/im")
}
let cfg = SDKConfig(appKey: file.appKey, debug: debug) let cfg = SDKConfig(appKey: file.appKey, debug: debug)
initialize(config: cfg) initialize(config: cfg)
} }