From e6b9dbf915fe26ddb4f312d3ac3837f1cda2ebde Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 26 Aug 2026 15:53:44 +0800 Subject: [PATCH] feat: add status pull task check (#98) --- android/app/build.gradle.kts | 5 +- .../agent/service/AgentForegroundService.kt | 53 ++++- .../goauto/agent/ui/AgentStatusFragment.kt | 206 ++++++++++++++---- .../agent/AccessibilityReadinessTest.kt | 22 ++ docs/02-architecture-and-code-map.md | 12 +- 5 files changed, 241 insertions(+), 57 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2ea820e..c8504f2 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 10 - versionName = "0.6.0" + versionCode = 11 + versionName = "0.7.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -49,6 +49,7 @@ android { dependencies { implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.fragment:fragment-ktx:1.8.5") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("com.google.android.material:material:1.12.0") testImplementation("junit:junit:4.13.2") diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt index 56fc306..3e9ef63 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt @@ -63,6 +63,7 @@ class AgentForegroundService : Service() { private val taskMutex = TaskExecutionMutex() private val runningTaskId = AtomicReference(null) private val working = AtomicBoolean(false) + private val manualCheckRequested = AtomicBoolean(false) private val registeredThisProcess = AtomicBoolean(false) private val taskWakeActive = AtomicBoolean(false) private val idleReturn = IdleReturnCoordinator() @@ -101,6 +102,7 @@ class AgentForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == ACTION_RECONNECT) registeredThisProcess.set(false) + if (intent?.action == ACTION_CHECK_NOW) manualCheckRequested.set(true) triggerSync() return START_STICKY } @@ -120,16 +122,19 @@ class AgentForegroundService : Service() { private fun triggerSync() { if (!working.compareAndSet(false, true)) return executor.execute { + val manualCheck = manualCheckRequested.getAndSet(false) try { - synchronizeAgent() + val outcome = synchronizeAgent() + if (manualCheck) publishManualCheckResult(outcome) } finally { working.set(false) + if (manualCheckRequested.get()) triggerSync() } } } - private fun synchronizeAgent() { - try { + private fun synchronizeAgent(): String { + return try { val serverUrl = ServerUrlPolicy.normalize(settingsStore.serverUrl(), BuildConfig.DEBUG) val api = AgentApiClient(serverUrl) var credentials = identityStore.credentials() @@ -183,6 +188,7 @@ class AgentForegroundService : Service() { tokenStored = storedCredentials != null, ) updateNotification(if (authenticationError) "设备认证失败" else "连接失败,等待网络恢复") + if (authenticationError) MANUAL_AUTH_ERROR else MANUAL_NETWORK_ERROR } catch (error: Exception) { cancelIdleReturn("Agent 运行异常") val configured = settingsStore.serverUrl().isNotBlank() @@ -194,26 +200,27 @@ class AgentForegroundService : Service() { tokenStored = storedCredentials != null, ) updateNotification(if (configured) "运行异常" else "等待配置服务端") + if (configured) MANUAL_ERROR else MANUAL_CONFIG_REQUIRED } } - private fun scheduleTask(api: AgentApiClient, token: String) { - if (taskMutex.currentTaskId() != null) return + private fun scheduleTask(api: AgentApiClient, token: String): String { + if (taskMutex.currentTaskId() != null) return MANUAL_BUSY recoverInterruptedPurchases(api, token) flushPurchaseOutbox(api, token) val purchaseTask = api.nextPurchaseTask(token) if (purchaseTask != null) { cancelIdleReturn("收到新的采购任务") schedulePurchaseTask(api, purchaseTask, token) - return + return MANUAL_PURCHASE_TASK } val task = api.nextTask(token) if (task == null) { evaluateIdleReturn() - return + return MANUAL_EMPTY } cancelIdleReturn("收到新的采集任务") - if (!taskMutex.tryAcquire(task.taskId)) return + if (!taskMutex.tryAcquire(task.taskId)) return MANUAL_BUSY if (task.status == "running") runningTaskId.set(task.taskId) stateStore.setActiveTask(task.taskId, "collection") taskExecutor.execute { @@ -226,6 +233,14 @@ class AgentForegroundService : Service() { triggerSync() } } + return MANUAL_COLLECTION_TASK + } + + private fun publishManualCheckResult(result: String) { + sendBroadcast(Intent(ACTION_CHECK_RESULT).apply { + setPackage(packageName) + putExtra(EXTRA_CHECK_RESULT, result) + }) } private fun schedulePurchaseTask(api: AgentApiClient, task: PurchaseAgentTask, token: String) { @@ -669,6 +684,17 @@ class AgentForegroundService : Service() { companion object { const val ACTION_RECONNECT = "cn.ilapage.goauto.agent.RECONNECT" + const val ACTION_CHECK_NOW = "cn.ilapage.goauto.agent.CHECK_NOW" + const val ACTION_CHECK_RESULT = "cn.ilapage.goauto.agent.CHECK_RESULT" + const val EXTRA_CHECK_RESULT = "check_result" + const val MANUAL_EMPTY = "empty" + const val MANUAL_COLLECTION_TASK = "collection_task" + const val MANUAL_PURCHASE_TASK = "purchase_task" + const val MANUAL_BUSY = "busy" + const val MANUAL_CONFIG_REQUIRED = "config_required" + const val MANUAL_AUTH_ERROR = "auth_error" + const val MANUAL_NETWORK_ERROR = "network_error" + const val MANUAL_ERROR = "error" private const val CHANNEL_ID = "agent_connection" private const val NOTIFICATION_ID = 1001 private const val HEARTBEAT_SECONDS = 15L @@ -686,6 +712,17 @@ class AgentForegroundService : Service() { context.startService(intent) } } + + fun checkNow(context: Context) { + val intent = Intent(context, AgentForegroundService::class.java).apply { + action = ACTION_CHECK_NOW + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } } } diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/ui/AgentStatusFragment.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/ui/AgentStatusFragment.kt index 490f165..2e5d710 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/ui/AgentStatusFragment.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/ui/AgentStatusFragment.kt @@ -1,88 +1,152 @@ package cn.ilapage.goauto.agent.ui +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper -import android.provider.Settings import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import cn.ilapage.goauto.agent.R -import cn.ilapage.goauto.agent.identity.SecureDeviceStore +import cn.ilapage.goauto.agent.service.AgentForegroundService import cn.ilapage.goauto.agent.service.AgentSettingsStore import cn.ilapage.goauto.agent.service.AgentStateStore -import com.google.android.material.button.MaterialButton import java.text.DateFormat import java.util.Date +internal object ManualTaskCheckPolicy { + fun blockedMessage(stateCode: String, currentTaskId: Long?, readiness: AccessibilityReadiness): String? = when { + currentTaskId != null || stateCode == "BUSY" -> "当前任务执行中,无需重复检查" + readiness != AccessibilityReadiness.READY -> "请先到“设置”开启采集采购助手" + stateCode == "CONFIG_REQUIRED" -> "请先到“设置”配置服务地址" + stateCode == "AUTH_ERROR" -> "设备身份校验失败,请先检查设置" + else -> null + } + + fun resultMessage(result: String): String = when (result) { + AgentForegroundService.MANUAL_EMPTY -> "暂无新任务,Agent 会继续自动检查" + AgentForegroundService.MANUAL_COLLECTION_TASK -> "已领取采集任务" + AgentForegroundService.MANUAL_PURCHASE_TASK -> "已领取采购任务" + AgentForegroundService.MANUAL_BUSY -> "当前任务执行中,无需重复检查" + AgentForegroundService.MANUAL_CONFIG_REQUIRED -> "请先到“设置”配置服务地址" + AgentForegroundService.MANUAL_AUTH_ERROR -> "设备身份校验失败,请先检查设置" + AgentForegroundService.MANUAL_NETWORK_ERROR -> "检查失败,Agent 将继续自动重试" + else -> "检查失败,Agent 将继续自动重试" + } +} + class AgentStatusFragment : Fragment() { private val handler = Handler(Looper.getMainLooper()) private lateinit var stateStore: AgentStateStore private lateinit var settingsStore: AgentSettingsStore - private lateinit var identityStore: SecureDeviceStore + private lateinit var refreshLayout: SwipeRefreshLayout + private lateinit var manualFeedback: TextView private lateinit var connectionTitle: TextView private lateinit var connectionDetail: TextView private lateinit var taskText: TextView private lateinit var accessibilityTitle: TextView private lateinit var accessibilityDetail: TextView private lateinit var deviceText: TextView + private var receiverRegistered = false + private val refresh = object : Runnable { override fun run() { refreshStatus() handler.postDelayed(this, REFRESH_MILLIS) } } + private val manualTimeout = Runnable { + if (::refreshLayout.isInitialized && refreshLayout.isRefreshing) { + finishManualCheck("检查超时,Agent 将继续自动重试", error = true) + } + } + private val hideFeedback = Runnable { + if (::manualFeedback.isInitialized && !refreshLayout.isRefreshing) manualFeedback.visibility = View.GONE + } + private val checkReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action != AgentForegroundService.ACTION_CHECK_RESULT) return + val result = intent.getStringExtra(AgentForegroundService.EXTRA_CHECK_RESULT).orEmpty() + finishManualCheck( + ManualTaskCheckPolicy.resultMessage(result), + error = result in setOf( + AgentForegroundService.MANUAL_AUTH_ERROR, + AgentForegroundService.MANUAL_CONFIG_REQUIRED, + AgentForegroundService.MANUAL_NETWORK_ERROR, + AgentForegroundService.MANUAL_ERROR, + ), + ) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stateStore = AgentStateStore(requireContext()) settingsStore = AgentSettingsStore(requireContext()) - identityStore = SecureDeviceStore(requireContext()) } override fun onCreateView(inflater: android.view.LayoutInflater, container: ViewGroup?, state: Bundle?): View { val context = requireContext() val content = context.column().apply { - addView(context.screenTitle("设备状态", "查看连接、任务与无障碍是否就绪")) + addView(context.screenTitle("设备状态", "在页面顶部下拉可立即检查新任务")) + deviceText = context.label("正在读取设备信息", 13f, context.getColor(R.color.agent_text_muted), true) + addView(deviceText, deviceText.fullWidth()) + manualFeedback = context.label("", 14f, context.getColor(R.color.agent_primary_light), true).apply { + visibility = View.GONE + setPadding(0, context.dp(12), 0, context.dp(12)) + } + addView(manualFeedback) addView(context.card(context.cardColumn().apply { - addView(context.label("服务端连接", 14f, context.getColor(R.color.agent_text_muted))) connectionTitle = context.label("正在读取", 21f, context.getColor(R.color.agent_warning), true) - connectionDetail = context.label("—", 14f, context.getColor(R.color.agent_text_muted)) - connectionDetail.setPadding(0, context.dp(6), 0, 0) + connectionDetail = context.label("—", 14f, context.getColor(R.color.agent_text_muted)).apply { + setPadding(0, context.dp(6), 0, 0) + } addView(connectionTitle) addView(connectionDetail) })) addView(context.card(context.cardColumn().apply { addView(context.label("当前任务", 14f, context.getColor(R.color.agent_text_muted))) - taskText = context.label("设备空闲", 18f, context.getColor(R.color.agent_text), true) - taskText.setPadding(0, context.dp(6), 0, 0) + taskText = context.label("等待任务", 18f, context.getColor(R.color.agent_text), true).apply { + setPadding(0, context.dp(6), 0, 0) + } addView(taskText) })) addView(context.card(context.cardColumn().apply { - addView(context.label("无障碍服务", 14f, context.getColor(R.color.agent_text_muted))) + addView(context.label("无障碍", 14f, context.getColor(R.color.agent_text_muted))) accessibilityTitle = context.label("正在检查", 18f, context.getColor(R.color.agent_warning), true) - accessibilityDetail = context.label("—", 14f, context.getColor(R.color.agent_text_muted)) - accessibilityDetail.setPadding(0, context.dp(6), 0, 0) + accessibilityDetail = context.label("—", 14f, context.getColor(R.color.agent_text_muted)).apply { + setPadding(0, context.dp(6), 0, 0) + } addView(accessibilityTitle) addView(accessibilityDetail) - addView(MaterialButton(context).apply { - text = "打开无障碍设置" - contentDescription = "打开系统无障碍设置" - minHeight = context.dp(48) - setOnClickListener { startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) } - }, fullWidth(12)) - })) - addView(context.card(context.cardColumn().apply { - addView(context.label("设备信息", 14f, context.getColor(R.color.agent_text_muted))) - deviceText = context.label("—", 14f, context.getColor(R.color.agent_text)) - deviceText.setPadding(0, context.dp(6), 0, 0) - addView(deviceText) })) } - return context.page(content) + refreshLayout = SwipeRefreshLayout(context).apply { + setColorSchemeResources(R.color.agent_primary_light) + setProgressBackgroundColorSchemeResource(R.color.agent_surface) + setOnRefreshListener(::checkNow) + addView(context.page(content), ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + )) + } + return refreshLayout + } + + override fun onStart() { + super.onStart() + registerCheckReceiver() + } + + override fun onStop() { + unregisterCheckReceiver() + super.onStop() } override fun onResume() { @@ -98,56 +162,108 @@ class AgentStatusFragment : Fragment() { override fun onDestroyView() { handler.removeCallbacks(refresh) + handler.removeCallbacks(manualTimeout) + handler.removeCallbacks(hideFeedback) super.onDestroyView() } + private fun checkNow() { + val state = stateStore.read() + val blocked = ManualTaskCheckPolicy.blockedMessage( + state.code, + state.currentTaskId, + AccessibilityReadinessDetector.current(requireContext()), + ) + if (blocked != null) { + finishManualCheck(blocked, error = true) + return + } + showManualFeedback("正在检查新任务…", error = false) + handler.removeCallbacks(manualTimeout) + handler.postDelayed(manualTimeout, MANUAL_TIMEOUT_MILLIS) + AgentForegroundService.checkNow(requireContext()) + } + + private fun finishManualCheck(message: String, error: Boolean) { + handler.removeCallbacks(manualTimeout) + if (::refreshLayout.isInitialized) refreshLayout.isRefreshing = false + showManualFeedback(message, error) + refreshStatus() + handler.removeCallbacks(hideFeedback) + handler.postDelayed(hideFeedback, FEEDBACK_MILLIS) + } + + private fun showManualFeedback(message: String, error: Boolean) { + manualFeedback.text = message + manualFeedback.setTextColor(requireContext().getColor(if (error) R.color.agent_warning else R.color.agent_primary_light)) + manualFeedback.visibility = View.VISIBLE + manualFeedback.announceForAccessibility(message) + } + private fun refreshStatus() { if (!isAdded || view == null) return val context = requireContext() val state = stateStore.read() val connected = state.code in setOf("ONLINE", "BUSY") connectionTitle.text = when (state.code) { - "ONLINE" -> "已连接 · 设备空闲" - "BUSY" -> "已连接 · 正在执行" + "ONLINE" -> "在线 · 空闲" + "BUSY" -> "在线 · 执行中" "CONNECTING" -> "正在连接" - "CONFIG_REQUIRED" -> "等待配置服务端" - "AUTH_ERROR" -> "设备身份校验失败" + "CONFIG_REQUIRED" -> "等待配置" + "AUTH_ERROR" -> "身份校验失败" else -> "连接异常" } connectionTitle.setTextColor(context.getColor(if (connected) R.color.agent_primary_light else R.color.agent_warning)) val heartbeat = state.lastHeartbeatAt.takeIf { it > 0L }?.let { DateFormat.getDateTimeInstance().format(Date(it)) - } ?: "尚无成功心跳" + } ?: "尚无心跳" connectionDetail.text = "${state.message}\n最近心跳:$heartbeat" taskText.text = state.currentTaskId?.let { id -> - val type = if (state.currentTaskType == "purchase") "采购" else "采集" - "正在执行${type}任务 ${if (type == "采购") "CG-" else "#"}$id" - } ?: "设备空闲,等待新任务" - + val purchase = state.currentTaskType == "purchase" + "${if (purchase) "采购" else "采集"}任务 ${if (purchase) "CG-" else "#"}$id" + } ?: "等待任务" when (AccessibilityReadinessDetector.current(context)) { AccessibilityReadiness.READY -> { - accessibilityTitle.text = "已开启并就绪" + accessibilityTitle.text = "已开启" accessibilityTitle.setTextColor(context.getColor(R.color.agent_primary_light)) - accessibilityDetail.text = "服务已绑定,可以执行采集和采购任务。" + accessibilityDetail.text = "可以执行任务" } AccessibilityReadiness.ENABLED_WAITING -> { - accessibilityTitle.text = "已开启,等待服务连接" + accessibilityTitle.text = "等待连接" accessibilityTitle.setTextColor(context.getColor(R.color.agent_warning)) - accessibilityDetail.text = "系统开关已打开,但 Agent 尚未连接服务。请返回本页稍候。" + accessibilityDetail.text = "请到“设置”检查无障碍服务" } AccessibilityReadiness.DISABLED -> { accessibilityTitle.text = "未开启" accessibilityTitle.setTextColor(context.getColor(R.color.agent_error)) - accessibilityDetail.text = "请打开 GoAuto 采集服务,否则任务会失败。" + accessibilityDetail.text = "请到“设置”开启" } } + val displayName = settingsStore.deviceName().ifBlank { "本机" } + deviceText.text = "$displayName · ${state.deviceId.takeIf { it > 0 }?.let { "#$it" } ?: "未注册"}" + } - val installId = runCatching { identityStore.installId() }.getOrElse { "读取失败" } - val displayName = settingsStore.deviceName().ifBlank { "使用手机型号" } - deviceText.text = "设备名称:$displayName\ninstallId:$installId\n设备编号:${state.deviceId.takeIf { it > 0 } ?: "未注册"}" + private fun registerCheckReceiver() { + if (receiverRegistered) return + val filter = IntentFilter(AgentForegroundService.ACTION_CHECK_RESULT) + if (Build.VERSION.SDK_INT >= 33) { + requireContext().registerReceiver(checkReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + @Suppress("DEPRECATION") + requireContext().registerReceiver(checkReceiver, filter) + } + receiverRegistered = true + } + + private fun unregisterCheckReceiver() { + if (!receiverRegistered) return + runCatching { requireContext().unregisterReceiver(checkReceiver) } + receiverRegistered = false } private companion object { const val REFRESH_MILLIS = 1_000L + const val MANUAL_TIMEOUT_MILLIS = 20_000L + const val FEEDBACK_MILLIS = 3_000L } } diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/AccessibilityReadinessTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/AccessibilityReadinessTest.kt index b59b6a6..6d5c770 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/AccessibilityReadinessTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/AccessibilityReadinessTest.kt @@ -2,6 +2,7 @@ package cn.ilapage.goauto.agent import cn.ilapage.goauto.agent.ui.AccessibilityReadiness import cn.ilapage.goauto.agent.ui.AccessibilityReadinessResolver +import cn.ilapage.goauto.agent.ui.ManualTaskCheckPolicy import cn.ilapage.goauto.agent.ui.SettingsAvailabilityResolver import org.junit.Assert.assertEquals import org.junit.Test @@ -37,4 +38,25 @@ class AccessibilityReadinessTest { assertEquals(false, SettingsAvailabilityResolver.editable("ONLINE", null, testing = true)) assertEquals(true, SettingsAvailabilityResolver.editable("ONLINE", null, testing = false)) } + + @Test + fun manualCheckExplainsBusyAndAccessibilityBlocks() { + assertEquals( + "当前任务执行中,无需重复检查", + ManualTaskCheckPolicy.blockedMessage("BUSY", 35L, AccessibilityReadiness.READY), + ) + assertEquals( + "请先到“设置”开启采集采购助手", + ManualTaskCheckPolicy.blockedMessage("ONLINE", null, AccessibilityReadiness.DISABLED), + ) + assertEquals(null, ManualTaskCheckPolicy.blockedMessage("ONLINE", null, AccessibilityReadiness.READY)) + } + + @Test + fun manualCheckUsesPlainResultMessages() { + assertEquals("暂无新任务,Agent 会继续自动检查", ManualTaskCheckPolicy.resultMessage("empty")) + assertEquals("已领取采集任务", ManualTaskCheckPolicy.resultMessage("collection_task")) + assertEquals("已领取采购任务", ManualTaskCheckPolicy.resultMessage("purchase_task")) + assertEquals("检查失败,Agent 将继续自动重试", ManualTaskCheckPolicy.resultMessage("network_error")) + } } diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index 74859e3..b061c74 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.- -wiki_revision: ca5286cc8b94d2338109552599da8a91b339127c -synchronized_at: 2026-08-26T06:52:02Z +wiki_revision: 18b8c41f545d2c2e883913a57647680a87a570b6 +synchronized_at: 2026-08-26T07:52:55Z # 架构与代码地图 @@ -191,3 +191,11 @@ Android Portal/Agent - Android 8.0 及以上的 `GoAutoAccessibilityService` 请求系统 `flagRequestAccessibilityButton`,服务连接后向 `AccessibilityButtonController` 注册单一回调,销毁时注销;服务重连不会叠加回调。 - 单击由系统分配给 GoAuto 的辅助功能按钮时,只调用既有 `openAgentStatus()`,以前台 `MainActivity` 打开“状态”Tab;不领取、创建、重置或重试任务,不打开 PDD,也不改变 15 秒空闲返回规则。 - 不支持或未分配系统辅助功能按钮的 ROM 保持安全降级;项目不创建自定义悬浮窗。 + +## Android Agent 状态页手动检查(#98) + +- `AgentStatusFragment` 使用 Android 原生下拉刷新容器;只有状态页顶部下拉会请求一次立即检查,采集/采购历史页的下拉只刷新记录,两者语义不同。 +- `AgentForegroundService.checkNow` 只向既有单线程同步与调度入口提交一个可合并请求;不建立第二套轮询器,也不改变 15 秒自动轮询、采购优先、设备互斥或服务端租约。 +- 手动请求在已有同步运行时保留一个待处理标记,当前同步结束后再执行一次;重复手势不会并发请求。 +- 结果通过仅限当前应用包的广播返回状态页,区分无任务、采集任务、采购任务、设备忙、配置/认证和网络错误。状态页无障碍信息只读,唯一系统设置入口保留在设置 Tab。 +- 手动检查不创建、重置或重试任务,不绕过无障碍与身份校验,不打开 PDD,不修改地址、不创建订单、不支付。