Browse Source

优化 移动代码

newdev_shunjiawei
liwei1dao 6 months ago
parent
commit
42e7755ffc
  1. 17
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt
  2. 13
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  3. 11
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  4. 118
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/StsAgent.swift

17
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt

@ -162,6 +162,9 @@ class StsAgent(
var credentialReady = false
private set
// ── 心跳冷却 ──────────────────────────────────────────────────────────────
private val HEARTBEAT_COOLDOWN_MS = 3_600_000L // 1 小时
// ═════════════════════════════════════════════════════════════════════════
// 凭证获取
// ═════════════════════════════════════════════════════════════════════════
@ -225,6 +228,16 @@ class StsAgent(
Log.w(TAG, "[YIDONG] reportHeartbeat: 凭证未就绪,跳过上报")
return
}
// 1 小时冷却:StsAgent 会被反复重建,init/fetchCredentials 都会调到这里,
// 必须在原生层拦,否则每次重建都会打一发心跳
val prefs = context.getSharedPreferences("sts_agent", Context.MODE_PRIVATE)
val last = prefs.getLong("last_heartbeat_ts", 0L)
val now = System.currentTimeMillis()
if (last > 0L && now - last < HEARTBEAT_COOLDOWN_MS) {
Log.i(TAG, "[YIDONG] reportHeartbeat: 距上次 ${(now - last) / 1000}s,冷却中跳过")
return
}
prefs.edit().putLong("last_heartbeat_ts", now).apply()
Log.i(TAG, "[YIDONG] reportHeartbeat: 开始上报 deviceId=${config.deviceId} deviceNo=${config.deviceNo} cmei=${config.cmei}")
launch {
try {
@ -271,10 +284,14 @@ class StsAgent(
emit("heartbeat_reported", mapOf("time" to System.currentTimeMillis()))
} else {
Log.e(TAG, "[YIDONG] reportHeartbeat: 上报失败 code=$code msg=$msg")
// 上报失败回滚时间戳,避免卡 1 小时无法重试
prefs.edit().putLong("last_heartbeat_ts", last).apply()
}
}
} catch (e: Exception) {
Log.e(TAG, "[YIDONG] reportHeartbeat: 请求异常 ${e.message}")
// 异常同样回滚时间戳
prefs.edit().putLong("last_heartbeat_ts", last).apply()
}
}
}

13
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -211,7 +211,8 @@ class AgentServiceImpl: NSObject {
deviceNo: config["stsDeviceNo"] as? String ?? "",
deviceId: config["stsDeviceId"] as? String ?? "",
deviceSecret: config["stsDeviceSecret"] as? String ?? "",
agentId: agentId
agentId: agentId,
cmei: config["stsCmei"] as? String ?? ""
)
stsAgent = StsAgent(
config: stsConfig,
@ -726,6 +727,16 @@ class AgentServiceImpl: NSObject {
return success
}
/// STS 模式下主动触发中国移动平台的凭证获取(Dart AgentService.fetchCredentials 会调到这里)
func fetchCredentials() {
stsAgent?.fetchCredentials()
}
/// STS 模式下主动上报一次心跳(1 小时冷却在原生层拦截)
func reportHeartbeat() {
stsAgent?.reportHeartbeat()
}
func interruptCurrentResponse() -> Bool {
if agentMode == "sts" {
stsAgent?.interruptSpeaking()

11
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -203,6 +203,17 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
}
impl.setCanWakeUp(canWakeUp)
result(true)
case "reportHeartbeat":
// 中国移动精灵心跳上报,原生层持久化 1 小时冷却
impl.reportHeartbeat()
result(nil)
case "fetchCredentials":
// 中国移动精灵凭证拉取,成功后会自动触发心跳(受冷却保护)
impl.fetchCredentials()
result(nil)
default:
result(FlutterMethodNotImplemented)
}

118
local_plugins/agent_service/ios/agent_service/Sources/agent_service/StsAgent.swift

@ -1,4 +1,5 @@
import Foundation
import UIKit
import AVFoundation
import CryptoKit
import os.log
@ -13,6 +14,7 @@ struct StsConfig {
var deviceId: String
var deviceSecret: String
let agentId: String
var cmei: String = ""
}
// MARK: - StsAgent
@ -71,6 +73,11 @@ class StsAgent: NSObject {
// MARK: 凭证请求
private var credentialsFetched = false
// MARK: 心跳冷却
/// 1 小时冷却(毫秒),避免 StsAgent 反复重建 / fetchCredentials 反复触发时频繁上报
private let HEARTBEAT_COOLDOWN_MS: Int64 = 3_600_000
private let HEARTBEAT_PREFS_KEY = "sts_agent_last_heartbeat_ts"
init(config: StsConfig,
onEvent: @escaping (String, [String: Any]) -> Void,
onCredentialsFetched: ((String, String, String) -> Void)? = nil) {
@ -80,6 +87,22 @@ class StsAgent: NSObject {
super.init()
let sessionConfig = URLSessionConfiguration.default
urlSession = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: OperationQueue())
os_log("StsAgent 初始化: productId=%{public}@ deviceNo=%{public}@ deviceId=%{public}@ cmei=%{public}@ hasSecret=%{public}@",
log: logger, type: .info,
config.productId, config.deviceNo, config.deviceId, config.cmei,
config.deviceSecret.isEmpty ? "false" : "true")
// 与 Android 行为对齐:凭证已有时立即尝试上报一次心跳(受 1 小时冷却保护)
if !config.deviceSecret.isEmpty && !config.deviceId.isEmpty {
credentialsFetched = true
reportHeartbeat()
}
}
/// 动态更新 CMEI(首次初始化时可能尚未获取到)
func updateCmei(_ cmei: String) {
guard !cmei.isEmpty, cmei != config.cmei else { return }
config.cmei = cmei
os_log("updateCmei: cmei 已更新为 %{public}@", log: logger, type: .info, cmei)
}
// MARK: - 公共接口
@ -139,8 +162,18 @@ class StsAgent: NSObject {
// MARK: - 凭证获取
/// 公开入口:主动触发认证检查(Dart 层 AgentService.fetchCredentials 会调用)
func fetchCredentials() {
fetchCredentialsIfNeeded { [weak self] in
// 成功与否都不做额外处理;fetchCredentialsIfNeeded 内部会在命中凭证时触发心跳
_ = self
}
}
private func fetchCredentialsIfNeeded(completion: @escaping () -> Void) {
if credentialsFetched && !config.deviceId.isEmpty && !config.deviceSecret.isEmpty {
// 凭证已有,直接上报心跳(受冷却保护)
reportHeartbeat()
completion()
return
}
@ -175,10 +208,95 @@ class StsAgent: NSObject {
if let deviceNo = body["deviceNo"] as? String { self.config.deviceNo = deviceNo }
self.credentialsFetched = true
self.onCredentialsFetched?(self.config.deviceId, self.config.deviceSecret, self.config.deviceNo)
// 凭证获取成功后立即上报心跳(受冷却保护)
self.reportHeartbeat()
completion()
}.resume()
}
// MARK: - 心跳上报
/// 向中国移动平台上报心跳;1 小时冷却通过 UserDefaults 持久化,所有调用路径共享同一把闸门
func reportHeartbeat() {
guard credentialsFetched,
!config.deviceId.isEmpty,
!config.deviceSecret.isEmpty else {
os_log("reportHeartbeat: 凭证未就绪,跳过上报", log: logger, type: .info)
return
}
let defaults = UserDefaults.standard
// 用 Double 存毫秒时间戳,避开 Int64 的 UserDefaults 桥接问题
let last = defaults.double(forKey: HEARTBEAT_PREFS_KEY)
let now = Date().timeIntervalSince1970 * 1000
if last > 0, now - last < Double(HEARTBEAT_COOLDOWN_MS) {
os_log("reportHeartbeat: 距上次 %{public}ds,冷却中跳过", log: logger, type: .info, Int((now - last) / 1000))
return
}
defaults.set(now, forKey: HEARTBEAT_PREFS_KEY)
os_log("reportHeartbeat: 开始上报 deviceId=%{public}@ deviceNo=%{public}@ cmei=%{public}@",
log: logger, type: .info, config.deviceId, config.deviceNo, config.cmei)
let urlStr = "https://ivs.chinamobiledevice.com:11443/v2/customer/device/report"
guard let url = URL(string: urlStr) else { return }
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.timeoutInterval = 10
let systemVersion = UIDevice.current.systemVersion
let params: [String: Any] = [
"innerIp": ["127.0.0.1"],
"netSpeed": "0Mbps",
"netType": "WiFi",
"platform": "iOS\(systemVersion)",
"sdkVersion": "ai_http_1.0",
"firmwareVersion": "1.0.0",
"imei": "",
"cmei": config.cmei,
"mac": "",
]
let bodyDict: [String: Any] = [
"deviceId": config.deviceId,
"deviceSecret": config.deviceSecret,
"productId": config.productId,
"productKey": config.productKey,
"params": params,
]
guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyDict, options: []) else { return }
req.httpBody = bodyData
URLSession.shared.dataTask(with: req) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
os_log("reportHeartbeat: 请求异常 %{public}@", log: self.logger, type: .error, error.localizedDescription)
// 异常回滚时间戳,避免卡 1 小时无法重试
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY)
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
os_log("reportHeartbeat: 响应解析失败", log: self.logger, type: .error)
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY)
return
}
// code 可能是 String 或 NSNumber,统一转成字符串
let code: String = {
if let s = json["code"] as? String { return s }
if let n = json["code"] as? NSNumber { return n.stringValue }
return ""
}()
let msg = (json["message"] as? String) ?? ""
if code == "200" {
os_log("reportHeartbeat: 上报成功 code=%{public}@ msg=%{public}@", log: self.logger, type: .info, code, msg)
self.emit("heartbeat_reported", data: ["time": Int64(now)])
} else {
os_log("reportHeartbeat: 上报失败 code=%{public}@ msg=%{public}@", log: self.logger, type: .error, code, msg)
// 上报失败回滚时间戳
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY)
}
}.resume()
}
// MARK: - WebSocket 连接
private func connect() {

Loading…
Cancel
Save