feat(android): add Pinduoduo search probe

This commit is contained in:
QiuSW
2026-07-25 19:40:19 +08:00
parent 1c1c158149
commit f226d0f2ff
21 changed files with 1356 additions and 70 deletions
@@ -42,15 +42,23 @@ import com.roubao.autopilot.vlm.GUIOwlClient
import com.roubao.autopilot.vlm.MAIUIClient
import com.roubao.autopilot.vlm.VLMClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import android.util.Log
import com.roubao.autopilot.pinduoduo.AndroidPinduoduoUiDriver
import com.roubao.autopilot.pinduoduo.PinduoduoSearchAutomation
import com.roubao.autopilot.pinduoduo.PinduoduoSearchWorkflow
import com.roubao.autopilot.workflow.WorkflowReport
import com.roubao.autopilot.workflow.WorkflowRunner
import com.roubao.autopilot.workflow.WorkflowState
private const val TAG = "MainActivity"
sealed class Screen(val route: String, val title: String, val icon: ImageVector, val selectedIcon: ImageVector) {
object Device : Screen("device", "设备", Icons.Outlined.Build, Icons.Filled.Build)
object Home : Screen("home", "肉包", Icons.Outlined.Home, Icons.Filled.Home)
object Home : Screen("home", "探针", Icons.Outlined.Search, Icons.Filled.Search)
object History : Screen("history", "记录", Icons.Outlined.List, Icons.Filled.List)
object Settings : Screen("settings", "设置", Icons.Outlined.Settings, Icons.Filled.Settings)
}
@@ -65,6 +73,11 @@ class MainActivity : ComponentActivity() {
private val mobileAgent = mutableStateOf<MobileAgent?>(null)
private var shizukuAvailable = mutableStateOf(false)
private val readinessSnapshot = mutableStateOf(DeviceReadinessSnapshot.empty())
private val searchProbeState = mutableStateOf(WorkflowState.IDLE)
private val searchProbeStepId = mutableStateOf<String?>(null)
private val searchProbeReport = mutableStateOf<WorkflowReport?>(null)
private var searchProbeRunner: WorkflowRunner? = null
private var searchProbeJob: Job? = null
// 当前执行的协程 Job(用于停止任务)
private var currentExecutionJob: kotlinx.coroutines.Job? = null
@@ -172,21 +185,19 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainApp() {
var currentScreen by remember { mutableStateOf<Screen>(Screen.Device) }
var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }
var selectedRecord by remember { mutableStateOf<ExecutionRecord?>(null) }
var showShizukuHelpDialog by remember { mutableStateOf(false) }
val settings by settingsManager.settings.collectAsState()
val colors = BaoziTheme.colors
val agent = mobileAgent.value
val agentState by agent?.state?.collectAsState() ?: remember { mutableStateOf(null) }
val logs by agent?.logs?.collectAsState() ?: remember { mutableStateOf(emptyList<String>()) }
val records by remember { executionRecords }
val isShizukuAvailable = shizukuAvailable.value && checkShizukuPermission()
val executing by remember { isExecuting }
val navigateToRecord by remember { shouldNavigateToRecord }
val recordId by remember { currentRecordId }
val readiness by remember { readinessSnapshot }
val probeState by remember { searchProbeState }
val probeStepId by remember { searchProbeStepId }
val probeReport by remember { searchProbeReport }
// 监听跳转事件
LaunchedEffect(navigateToRecord, recordId) {
@@ -211,7 +222,7 @@ class MainActivity : ComponentActivity() {
contentColor = colors.textPrimary,
tonalElevation = 0.dp
) {
listOf(Screen.Device, Screen.Home, Screen.History, Screen.Settings).forEach { screen ->
listOf(Screen.Home, Screen.Device, Screen.History, Screen.Settings).forEach { screen ->
val selected = currentScreen == screen
NavigationBarItem(
icon = {
@@ -270,35 +281,14 @@ class MainActivity : ComponentActivity() {
},
onOpenPinduoduo = { openPinduoduo() }
)
Screen.Home -> {
// 每次进入首页都检测 Shizuku 状态
LaunchedEffect(Unit) {
checkAndUpdateShizukuStatus()
}
HomeScreen(
agentState = agentState,
logs = logs,
onExecute = { instruction ->
runAgent(
instruction = instruction,
apiKey = settings.apiKey,
baseUrl = settings.baseUrl,
model = settings.model,
maxSteps = settings.maxSteps,
isGUIAgent = settings.currentProvider.isGUIAgent,
providerId = settings.currentProviderId
)
},
onStop = {
mobileAgent.value?.stop()
},
shizukuAvailable = isShizukuAvailable,
currentModel = settings.model,
onRefreshShizuku = { refreshShizukuStatus() },
onShizukuRequired = { showShizukuHelpDialog = true },
isExecuting = executing
)
}
Screen.Home -> SearchProbeScreen(
readiness = readiness,
state = probeState,
currentStepId = probeStepId,
report = probeReport,
onStart = { startSearchProbe() },
onStop = { stopSearchProbe() }
)
Screen.History -> HistoryScreen(
records = records,
onRecordClick = { record -> selectedRecord = record },
@@ -340,10 +330,6 @@ class MainActivity : ComponentActivity() {
}
}
// Shizuku 帮助对话框
if (showShizukuHelpDialog) {
ShizukuHelpDialog(onDismiss = { showShizukuHelpDialog = false })
}
}
private fun deleteRecord(id: String) {
@@ -357,10 +343,15 @@ class MainActivity : ComponentActivity() {
super.onResume()
if (::readinessChecker.isInitialized) {
refreshReadiness()
lifecycleScope.launch {
delay(READINESS_RECHECK_DELAY_MS)
refreshReadiness()
}
}
}
override fun onDestroy() {
searchProbeRunner?.requestStop()
super.onDestroy()
Shizuku.removeBinderReceivedListener(binderReceivedListener)
Shizuku.removeBinderDeadListener(binderDeadListener)
@@ -382,6 +373,48 @@ class MainActivity : ComponentActivity() {
startActivity(launchIntent)
}
private fun startSearchProbe() {
val readiness = readinessChecker.snapshot()
readinessSnapshot.value = readiness
if (!readiness.canStartProbe) {
Toast.makeText(this, "设备检查存在阻塞项", Toast.LENGTH_SHORT).show()
return
}
if (searchProbeJob?.isActive == true) {
return
}
val runner = WorkflowRunner(
PinduoduoSearchAutomation(AndroidPinduoduoUiDriver(this))
)
searchProbeRunner = runner
searchProbeReport.value = null
searchProbeState.value = WorkflowState.IDLE
searchProbeStepId.value = null
searchProbeJob = lifecycleScope.launch {
val stateCollector = launch {
runner.state.collect { state -> searchProbeState.value = state }
}
val stepCollector = launch {
runner.currentStepId.collect { stepId -> searchProbeStepId.value = stepId }
}
try {
val report = runner.run(PinduoduoSearchWorkflow.steps())
searchProbeReport.value = report
searchProbeState.value = report.state
} finally {
stateCollector.cancel()
stepCollector.cancel()
searchProbeRunner = null
refreshReadiness()
}
}
}
private fun stopSearchProbe() {
searchProbeRunner?.requestStop()
}
private fun checkShizukuPermission(): Boolean {
return try {
val granted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
@@ -460,6 +493,10 @@ class MainActivity : ComponentActivity() {
}
}
private companion object {
const val READINESS_RECHECK_DELAY_MS = 750L
}
private fun runAgent(
instruction: String,
apiKey: String,