fix: persist pairing token before agent startup

This commit is contained in:
Manus AI
2026-08-08 22:03:04 +08:00
parent e36b05159b
commit fbea77f32d
4 changed files with 200 additions and 32 deletions

View File

@@ -1,11 +1,18 @@
package com.system.cloudservice.ui package com.system.cloudservice.ui
import android.Manifest import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -16,6 +23,8 @@ import com.google.gson.reflect.TypeToken
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import com.system.cloudservice.BuildConfig import com.system.cloudservice.BuildConfig
import com.system.cloudservice.R import com.system.cloudservice.R
import com.system.cloudservice.engine.ConnectionState
import com.system.cloudservice.frida.FridaConfig
import com.system.cloudservice.engine.LanDiscovery import com.system.cloudservice.engine.LanDiscovery
import com.system.cloudservice.service.AgentForegroundService import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo import com.system.cloudservice.util.DeviceInfo
@@ -30,10 +39,17 @@ class SetupActivity : AppCompatActivity() {
private enum class PendingAction { NONE, START_QR_SCAN } private enum class PendingAction { NONE, START_QR_SCAN }
private var pendingAction: PendingAction = PendingAction.NONE private var pendingAction: PendingAction = PendingAction.NONE
private lateinit var tvStatus: TextView private lateinit var viewFlipper: ViewFlipper
private lateinit var tvStepIndicator: TextView
private lateinit var tvNetworkStatus: TextView
private lateinit var btnStep1Next: Button
private lateinit var tvSetupStatus: TextView
private lateinit var btnScan: Button private lateinit var btnScan: Button
private lateinit var btnManualInput: Button private lateinit var btnManualInput: Button
private lateinit var progressBar: ProgressBar private lateinit var progressBar: ProgressBar
private lateinit var tvSuccessDeviceId: TextView
private var currentStep = 1
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -44,16 +60,69 @@ class SetupActivity : AppCompatActivity() {
if (!hasForcedConfig && isAlreadyBound()) { launchMain(); return } if (!hasForcedConfig && isAlreadyBound()) { launchMain(); return }
setContentView(R.layout.activity_setup) setContentView(R.layout.activity_setup)
tvStatus = findViewById(R.id.tvSetupStatus)
viewFlipper = findViewById(R.id.viewFlipper)
tvStepIndicator = findViewById(R.id.tvStepIndicator)
tvNetworkStatus = findViewById(R.id.tvNetworkStatus)
btnStep1Next = findViewById(R.id.btnStep1Next)
tvSetupStatus = findViewById(R.id.tvSetupStatus)
btnScan = findViewById(R.id.btnScan) btnScan = findViewById(R.id.btnScan)
btnManualInput = findViewById(R.id.btnManualInput) btnManualInput = findViewById(R.id.btnManualInput)
progressBar = findViewById(R.id.progressSetup) progressBar = findViewById(R.id.progressSetup)
tvSuccessDeviceId = findViewById(R.id.tvSuccessDeviceId)
// 步骤 1联网检查
btnStep1Next.setOnClickListener { goToStep(2) }
// 步骤 2保活 checklistWP-AGENT-01
findViewById<Button>(R.id.btnSetupAutoStart).setOnClickListener {
safeStart(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:$packageName")
})
}
findViewById<Button>(R.id.btnSetupBattery).setOnClickListener {
safeStart(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
}
findViewById<Button>(R.id.btnSetupNotify).setOnClickListener {
safeStart(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
})
}
findViewById<Button>(R.id.btnSetupA11y).setOnClickListener {
safeStart(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
findViewById<Button>(R.id.btnStep2Next).setOnClickListener { goToStep(3) }
// 步骤 3扫码绑定
btnScan.setOnClickListener { ensureCameraThenStartQr() } btnScan.setOnClickListener { ensureCameraThenStartQr() }
btnManualInput.setOnClickListener { startLanScan() } btnManualInput.setOnClickListener { startLanScan() }
// 成功页
findViewById<Button>(R.id.btnCopySuccess).setOnClickListener { copyDeviceId(tvSuccessDeviceId.text.toString()) }
findViewById<Button>(R.id.btnEnterMain).setOnClickListener { launchMain() }
// adb 注入配置:跳过向导直接绑定
if (hasForcedConfig && consumeIntentConfigIfPresent()) return if (hasForcedConfig && consumeIntentConfigIfPresent()) return
if (consumeIntentConfigIfPresent()) return if (consumeIntentConfigIfPresent()) return
autoScanLan() if (isAlreadyBound()) {
launchMain()
finish()
return
}
goToStep(1)
checkNetwork()
}
override fun onResume() {
super.onResume()
if (currentStep == 1 && ::tvNetworkStatus.isInitialized) checkNetwork()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
if (::tvSetupStatus.isInitialized) consumeIntentConfigIfPresent()
} }
override fun onDestroy() { super.onDestroy(); scope.cancel() } override fun onDestroy() { super.onDestroy(); scope.cancel() }
@@ -61,13 +130,43 @@ class SetupActivity : AppCompatActivity() {
private fun isAlreadyBound(): Boolean = private fun isAlreadyBound(): Boolean =
prefs.getBoolean("bound", false) && prefs.getString("server_url", "").orEmpty().isNotBlank() prefs.getBoolean("bound", false) && prefs.getString("server_url", "").orEmpty().isNotBlank()
/** 步骤切换1 联网 / 2 保活 / 3 扫码 / 4 成功 */
private fun goToStep(step: Int) {
currentStep = step
viewFlipper.displayedChild = step - 1
tvStepIndicator.text = when (step) {
1 -> "1/3 · 联网检查"
2 -> "2/3 · 保活权限"
3 -> "3/3 · 扫码绑定"
else -> "绑定完成"
}
if (step == 3) autoScanLan()
}
/** 步骤 1 联网检查WiFi/移动网络可达即放行 */
private fun checkNetwork() {
scope.launch {
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetwork
val caps = cm.getNetworkCapabilities(network)
val hasNetwork = caps != null && (
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
)
if (hasNetwork) {
tvNetworkStatus.text = "网络已连接 ✓\n请点击「下一步」继续"
btnStep1Next.isEnabled = true
} else {
tvNetworkStatus.text = "未检测到网络连接\n请连接 WiFi 后返回本页"
btnStep1Next.isEnabled = false
}
}
}
private fun ensureCameraThenStartQr() { private fun ensureCameraThenStartQr() {
val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
if (granted) { if (granted) { startQrScan(); return }
startQrScan()
return
}
pendingAction = PendingAction.START_QR_SCAN pendingAction = PendingAction.START_QR_SCAN
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 2001) ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 2001)
} }
@@ -75,6 +174,7 @@ class SetupActivity : AppCompatActivity() {
private fun startQrScan() { private fun startQrScan() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
IntentIntegrator(this) IntentIntegrator(this)
.setCaptureActivity(PortraitCaptureActivity::class.java)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.setPrompt("对准服务器二维码") .setPrompt("对准服务器二维码")
.setCameraId(0) .setCameraId(0)
@@ -93,14 +193,12 @@ class SetupActivity : AppCompatActivity() {
) { ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != 2001) return if (requestCode != 2001) return
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (!granted) { if (!granted) {
tvStatus.text = "需要相机权限才能扫码绑定" tvSetupStatus.text = "需要相机权限才能扫码绑定"
pendingAction = PendingAction.NONE pendingAction = PendingAction.NONE
return return
} }
when (pendingAction) { when (pendingAction) {
PendingAction.START_QR_SCAN -> startQrScan() PendingAction.START_QR_SCAN -> startQrScan()
PendingAction.NONE -> {} PendingAction.NONE -> {}
@@ -109,7 +207,7 @@ class SetupActivity : AppCompatActivity() {
} }
private fun autoScanLan() { private fun autoScanLan() {
tvStatus.text = "正在搜索局域网服务器..." tvSetupStatus.text = "正在搜索局域网服务器..."
progressBar.visibility = View.VISIBLE progressBar.visibility = View.VISIBLE
scope.launch { scope.launch {
try { try {
@@ -117,29 +215,29 @@ class SetupActivity : AppCompatActivity() {
val servers = LanDiscovery.discover(localIp) val servers = LanDiscovery.discover(localIp)
if (servers.isNotEmpty()) { if (servers.isNotEmpty()) {
val best = servers.first() val best = servers.first()
tvStatus.text = "发现服务器: ${best.name}" tvSetupStatus.text = "发现服务器: ${best.name}"
handleConfig(mapOf("server" to best.wsUrl, "project" to "cunkebao")) handleConfig(mapOf("server" to best.wsUrl, "project" to "cunkebao"))
} else { } else {
tvStatus.text = "未发现局域网服务器\n请扫码或搜索连接" tvSetupStatus.text = "未发现局域网服务器\n请扫码绑定"
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("LAN auto scan failed", e) Logger.e("LAN auto scan failed", e)
tvStatus.text = "请扫码绑定服务器" tvSetupStatus.text = "请扫码绑定服务器"
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
} }
} }
} }
private fun startLanScan() { private fun startLanScan() {
tvStatus.text = "搜索局域网服务器中..." tvSetupStatus.text = "搜索局域网服务器中..."
setLoading(true) setLoading(true)
scope.launch { scope.launch {
try { try {
val localIp = withContext(Dispatchers.IO) { getLocalIp() } val localIp = withContext(Dispatchers.IO) { getLocalIp() }
val servers = LanDiscovery.discover(localIp) val servers = LanDiscovery.discover(localIp)
if (servers.isEmpty()) { if (servers.isEmpty()) {
tvStatus.text = "未找到服务器,请扫码绑定" tvSetupStatus.text = "未找到服务器,请扫码绑定"
setLoading(false) setLoading(false)
return@launch return@launch
} }
@@ -150,7 +248,7 @@ class SetupActivity : AppCompatActivity() {
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("LAN scan failed", e) Logger.e("LAN scan failed", e)
tvStatus.text = "搜索失败,请扫码绑定" tvSetupStatus.text = "搜索失败,请扫码绑定"
setLoading(false) setLoading(false)
} }
} }
@@ -177,9 +275,7 @@ class SetupActivity : AppCompatActivity() {
val addrs = ni.inetAddresses val addrs = ni.inetAddresses
while (addrs.hasMoreElements()) { while (addrs.hasMoreElements()) {
val addr = addrs.nextElement() val addr = addrs.nextElement()
if (!addr.isLoopbackAddress && addr is java.net.Inet4Address) { if (!addr.isLoopbackAddress && addr is java.net.Inet4Address) return addr.hostAddress
return addr.hostAddress
}
} }
} }
} catch (_: Exception) {} } catch (_: Exception) {}
@@ -201,7 +297,7 @@ class SetupActivity : AppCompatActivity() {
handleConfig(config) handleConfig(config)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("QR parse failed", e) Logger.e("QR parse failed", e)
tvStatus.text = "二维码格式错误,请重试" tvSetupStatus.text = "二维码格式错误,请重试"
} }
} }
@@ -209,44 +305,87 @@ class SetupActivity : AppCompatActivity() {
* 支持通过 adb am start 直接注入配置,避免手动扫码/局域网发现。 * 支持通过 adb am start 直接注入配置,避免手动扫码/局域网发现。
* 示例: * 示例:
* adb shell am start -n com.system.cloudservice/.ui.SetupActivity \ * adb shell am start -n com.system.cloudservice/.ui.SetupActivity \
* --es server_url ws://127.0.0.1:8899 --es project_id cunkebao * --es server_url ws://127.0.0.1:8899 --es project_id cunkebao \
* --es pairing_token '<token>' --es device_id xgfe65eimrrofyws --ei frida_port 15715
*/ */
private fun consumeIntentConfigIfPresent(): Boolean { private fun consumeIntentConfigIfPresent(): Boolean {
val server = intent?.getStringExtra("server_url")?.trim().orEmpty() val server = intent?.getStringExtra("server_url")?.trim().orEmpty()
if (server.isEmpty()) return false if (server.isEmpty()) return false
val project = intent?.getStringExtra("project_id")?.trim().takeUnless { it.isNullOrEmpty() } ?: "cunkebao" val project = intent?.getStringExtra("project_id")?.trim().takeUnless { it.isNullOrEmpty() } ?: "cunkebao"
val pwa = intent?.getStringExtra("pwa_url")?.trim().takeUnless { it.isNullOrEmpty() } ?: BuildConfig.PWA_URL val pwa = intent?.getStringExtra("pwa_url")?.trim().takeUnless { it.isNullOrEmpty() } ?: BuildConfig.PWA_URL
tvStatus.text = "收到外部配置,正在绑定..." val cfg = mutableMapOf("server" to server, "project" to project, "pwa" to pwa)
handleConfig(mapOf("server" to server, "project" to project, "pwa" to pwa)) intent?.getStringExtra("pairing_token")?.trim()?.takeIf { it.isNotEmpty() }?.let { token ->
cfg["pairing_token"] = token
}
intent?.getStringExtra("device_id")?.trim()?.takeIf { it.isNotEmpty() }?.let { deviceId ->
cfg["device_id"] = deviceId
}
if (intent?.hasExtra("frida_port") == true) {
cfg["frida_port"] = intent!!.getIntExtra("frida_port", FridaConfig.DEFAULT_PORT).toString()
}
if (::tvSetupStatus.isInitialized) tvSetupStatus.text = "收到外部配置,正在绑定..."
handleConfig(cfg)
return true return true
} }
private fun handleConfig(config: Map<String, String>) { private fun handleConfig(config: Map<String, String>) {
setLoading(true) setLoading(true)
tvStatus.text = "连接中..." tvSetupStatus.text = "连接中..."
scope.launch { scope.launch {
try { try {
val serverUrl = config["server"] ?: throw IllegalArgumentException("缺少服务器地址") val serverUrl = config["server"] ?: throw IllegalArgumentException("缺少服务器地址")
val projectId = config["project"] ?: "cunkebao" val projectId = config["project"] ?: "cunkebao"
val pwaUrl = config["pwa"] ?: BuildConfig.PWA_URL val pwaUrl = config["pwa"] ?: BuildConfig.PWA_URL
val deviceId = DeviceInfo.getOrCreateDeviceId(this@SetupActivity) val deviceId = config["device_id"]?.trim()?.takeIf { it.isNotEmpty() }
?: DeviceInfo.getOrCreateDeviceId(this@SetupActivity)
prefs.edit() // 绑定字段必须一次性同步落盘AgentEngine.start() 会立即读取 pairing_token
// 组装 WSS URL。使用 apply() 会产生竞态,导致首次扫码握手漏带 token。
val editor = prefs.edit()
.putString("server_url", serverUrl) .putString("server_url", serverUrl)
.putString("project_id", projectId) .putString("project_id", projectId)
.putString("device_id", deviceId) .putString("device_id", deviceId)
.putString("pwa_url", pwaUrl) .putString("pwa_url", pwaUrl)
.putBoolean("auto_connect", true) .putBoolean("auto_connect", true)
.putBoolean("bound", true) .putBoolean("bound", true)
.apply() config["ai_api_key"]?.trim()?.takeIf { it.isNotEmpty() }?.let { key ->
editor.putString("ai_api_key", key).putBoolean("ai_enabled", true)
}
config["ai_api_url"]?.trim()?.takeIf { it.isNotEmpty() }?.let { url ->
editor.putString("ai_api_url", url)
}
config["public_servers"]?.trim()?.takeIf { it.isNotEmpty() }?.let { ps ->
editor.putString("public_servers", ps)
}
config["pairing_token"]?.trim()?.takeIf { it.isNotEmpty() }?.let { token ->
editor.putString("pairing_token", token)
}
config["auto_discover"]?.trim()?.takeIf { it.isNotEmpty() }?.let { ad ->
editor.putBoolean("auto_discover", ad == "1" || ad.equals("true", true))
}
if (!editor.commit()) throw IllegalStateException("绑定配置保存失败")
config["frida_port"]?.trim()?.toIntOrNull()?.takeIf { it in 1025..65535 }?.let { port ->
FridaConfig.savePort(this@SetupActivity, port)
}
startAgentService(serverUrl, projectId, deviceId) startAgentService(serverUrl, projectId, deviceId)
delay(300) tvSuccessDeviceId.text = DeviceInfo.deviceIdMd5(this@SetupActivity)
launchMain() // BIND-04 连接阶段实时映射(复用 DashboardFragment 人话映射)
var elapsed = 0
while (isActive && elapsed < 15_000) {
delay(500)
elapsed += 500
val engine = AgentForegroundService.instance?.engine
val connState = engine?.wsManager?.state?.value
tvSetupStatus.text = "连接中 · ${stageText(engine?.getConnectStage())}..."
if (connState == ConnectionState.CONNECTED) { goToStep(4); return@launch }
}
tvSetupStatus.text = "连接超时,请检查服务器\n可返回首页查看连接状态"
setLoading(false)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Bind failed", e) Logger.e("Bind failed", e)
tvStatus.text = "连接失败,请重试" tvSetupStatus.text = "连接失败,请重试"
setLoading(false) setLoading(false)
} }
} }
@@ -258,6 +397,17 @@ class SetupActivity : AppCompatActivity() {
progressBar.visibility = if (loading) View.VISIBLE else View.GONE progressBar.visibility = if (loading) View.VISIBLE else View.GONE
} }
private fun copyDeviceId(deviceId: String) {
val cm = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(ClipData.newPlainText("device_id_md5", deviceId))
Toast.makeText(this, "已复制设备 MD5", Toast.LENGTH_SHORT).show()
}
private fun safeStart(intent: Intent) {
try { startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
catch (e: Exception) { Toast.makeText(this, "无法打开:${e.message}", Toast.LENGTH_SHORT).show() }
}
private fun startAgentService(serverUrl: String, projectId: String, deviceId: String) { private fun startAgentService(serverUrl: String, projectId: String, deviceId: String) {
val intent = Intent(this, AgentForegroundService::class.java).apply { val intent = Intent(this, AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_START action = AgentForegroundService.ACTION_START
@@ -270,4 +420,13 @@ class SetupActivity : AppCompatActivity() {
} }
private fun launchMain() { startActivity(Intent(this, MainActivity::class.java)); finish() } private fun launchMain() { startActivity(Intent(this, MainActivity::class.java)); finish() }
/** BIND-04 connect_stage 人话映射(与 DashboardFragment 一致) */
private fun stageText(raw: String?): String = when (raw) {
"primary" -> "主服务器"
"lan" -> "局域网服务器"
"public" -> "公网备用"
null -> "初始化"
else -> raw
}
} }

Binary file not shown.

View File

@@ -3799,4 +3799,7 @@ v3.1: Agent 内置 AI Brain → 心跳驱动自主决策 → Frida优先/u2兜
- 已把设备页 `扫描` 拆分为 `扫码绑定``扫描 ADB`;扫码绑定会自动生成当前服务器二维码。 - 已把设备页 `扫描` 拆分为 `扫码绑定``扫描 ADB`;扫码绑定会自动生成当前服务器二维码。
- 已新增离线设备删除接口和页面按钮,在线设备有 409 保护,保留命令审计。 - 已新增离线设备删除接口和页面按钮,在线设备有 409 保护,保留命令审计。
- 验收:本地定向测试 3/3 通过;宝塔容器 healthy、二维码接口 HTTP 200 且返回 PNG、页面标识回读通过、删除不存在设备返回 404。当前 WSS 在线设备 0 台,等待手机扫码注册。 - 验收:本地定向测试 3/3 通过;宝塔容器 healthy、二维码接口 HTTP 200 且返回 PNG、页面标识回读通过、删除不存在设备返回 404。当前 WSS 在线设备 0 台,等待手机扫码注册。
### 2026-08-08 22:00扫码成功但未进入设备列表修复
- 根因确认:扫码配置的 pairing_token 使用异步 apply 写入,与前台 Agent 启动存在首次握手竞态。
- 已改为原子 commit 后启动 Agent修复 APK 已发布到公网静态下载入口HTTP 200长度 6,663,970 字节SHA-256 已核验。

View File

@@ -194,4 +194,10 @@ curl -X POST http://127.0.0.1:8899/api/v3/health 2>/dev/null || true
- 页面源码回读确认 `openBindQRCode``扫码绑定``扫描 ADB``删除失效设备` 均已上线。 - 页面源码回读确认 `openBindQRCode``扫码绑定``扫描 ADB``删除失效设备` 均已上线。
- `DELETE /api/v3/devices/__verification_missing_device__` 返回 HTTP 404确认删除接口受设备存在性校验保护在线设备另有 HTTP 409 保护。 - `DELETE /api/v3/devices/__verification_missing_device__` 返回 HTTP 404确认删除接口受设备存在性校验保护在线设备另有 HTTP 409 保护。
- 公网健康检查:`https://wpsdk.quwanzhi.com/health` 返回 healthy当前 `devices_online=0`,等待手机扫描二维码并完成 WSS 注册。 - 公网健康检查:`https://wpsdk.quwanzhi.com/health` 返回 healthy当前 `devices_online=0`,等待手机扫描二维码并完成 WSS 注册。
### 2026-08-08 22:00 已扫码未注册修复
- **根因**:手机扫码后 `SetupActivity` 以异步 `SharedPreferences.apply()` 写入 `pairing_token`,随即启动 Agent首次启动存在令牌尚未落盘的竞态WSS 握手未带 token。
- **修复**:扫码绑定字段改为单个 Editor 原子 `commit()` 成功后再启动 `AgentForegroundService`
- **APK**`/static/downloads/workphone-agent-latest.apk`SHA-256`b50a96b9475e365a377e0ac9d7efbf40fba04ac357fb660001829946cc3ad601`
- **验收口径**:安装修复包后重新扫码;服务端日志应出现 WSS 连接与 `设备注册``/health``devices_online` 应大于 0。