feat(android): extract structured purchase requirements
This commit is contained in:
@@ -41,6 +41,16 @@ import androidx.core.view.WindowCompat
|
||||
import com.roubao.autopilot.vlm.GUIOwlClient
|
||||
import com.roubao.autopilot.vlm.MAIUIClient
|
||||
import com.roubao.autopilot.vlm.VLMClient
|
||||
import com.roubao.autopilot.task.RequirementProbeSource
|
||||
import com.roubao.autopilot.vlm.AndroidRequirementVlmGateway
|
||||
import com.roubao.autopilot.vlm.RequirementExtraction
|
||||
import com.roubao.autopilot.vlm.RequirementExtractionFailureCode
|
||||
import com.roubao.autopilot.vlm.RequirementExtractionInput
|
||||
import com.roubao.autopilot.vlm.RequirementExtractionResult
|
||||
import com.roubao.autopilot.vlm.RequirementExtractor
|
||||
import com.roubao.autopilot.vlm.RequirementProbeState
|
||||
import com.roubao.autopilot.vlm.RequirementProviderEndpointPolicy
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -74,6 +84,7 @@ class MainActivity : ComponentActivity() {
|
||||
private lateinit var settingsManager: SettingsManager
|
||||
private lateinit var executionRepository: ExecutionRepository
|
||||
private lateinit var readinessChecker: DeviceReadinessChecker
|
||||
private lateinit var requirementProbeSource: RequirementProbeSource
|
||||
|
||||
private val mobileAgent = mutableStateOf<MobileAgent?>(null)
|
||||
private var shizukuAvailable = mutableStateOf(false)
|
||||
@@ -83,8 +94,13 @@ class MainActivity : ComponentActivity() {
|
||||
private val searchProbeReport = mutableStateOf<WorkflowReport?>(null)
|
||||
private val candidateEvidence =
|
||||
mutableStateOf<List<PinduoduoCandidateEvidence>>(emptyList())
|
||||
private val requirementProbeState = mutableStateOf(RequirementProbeState.IDLE)
|
||||
private val requirementExtraction = mutableStateOf<RequirementExtraction?>(null)
|
||||
private val requirementFailureCode =
|
||||
mutableStateOf<RequirementExtractionFailureCode?>(null)
|
||||
private var searchProbeRunner: WorkflowRunner? = null
|
||||
private var searchProbeJob: Job? = null
|
||||
private var requirementProbeJob: Job? = null
|
||||
|
||||
// 当前执行的协程 Job(用于停止任务)
|
||||
private var currentExecutionJob: kotlinx.coroutines.Job? = null
|
||||
@@ -140,6 +156,7 @@ class MainActivity : ComponentActivity() {
|
||||
settingsManager = SettingsManager(this)
|
||||
executionRepository = ExecutionRepository(this)
|
||||
readinessChecker = DeviceReadinessChecker(this)
|
||||
requirementProbeSource = RequirementProbeSource(this)
|
||||
refreshReadiness()
|
||||
|
||||
// 加载执行记录
|
||||
@@ -206,6 +223,9 @@ class MainActivity : ComponentActivity() {
|
||||
val probeStepId by remember { searchProbeStepId }
|
||||
val probeReport by remember { searchProbeReport }
|
||||
val evidence by remember { candidateEvidence }
|
||||
val extractionState by remember { requirementProbeState }
|
||||
val extraction by remember { requirementExtraction }
|
||||
val extractionFailure by remember { requirementFailureCode }
|
||||
|
||||
// 监听跳转事件
|
||||
LaunchedEffect(navigateToRecord, recordId) {
|
||||
@@ -295,6 +315,11 @@ class MainActivity : ComponentActivity() {
|
||||
currentStepId = probeStepId,
|
||||
report = probeReport,
|
||||
candidateEvidenceCount = evidence.size,
|
||||
requirementState = extractionState,
|
||||
requirement = extraction,
|
||||
requirementFailureCode = extractionFailure,
|
||||
onStartRequirement = { startRequirementProbe() },
|
||||
onStopRequirement = { stopRequirementProbe() },
|
||||
onStart = { startSearchProbe() },
|
||||
onStop = { stopSearchProbe() }
|
||||
)
|
||||
@@ -361,6 +386,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
searchProbeRunner?.requestStop()
|
||||
requirementProbeJob?.cancel()
|
||||
super.onDestroy()
|
||||
Shizuku.removeBinderReceivedListener(binderReceivedListener)
|
||||
Shizuku.removeBinderDeadListener(binderDeadListener)
|
||||
@@ -448,6 +474,110 @@ class MainActivity : ComponentActivity() {
|
||||
searchProbeRunner?.requestStop()
|
||||
}
|
||||
|
||||
private fun startRequirementProbe() {
|
||||
if (requirementProbeJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
if (searchProbeJob?.isActive == true) {
|
||||
Toast.makeText(this, "请先停止候选探针", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
requirementExtraction.value = null
|
||||
requirementFailureCode.value = null
|
||||
|
||||
val settings = settingsManager.settings.value
|
||||
val provider = settings.currentProvider
|
||||
if (!provider.supportsRequirementExtraction) {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.PROVIDER_UNSUPPORTED)
|
||||
return
|
||||
}
|
||||
if (settings.baseUrl.isBlank() || settings.model.isBlank()) {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.PROVIDER_NOT_CONFIGURED)
|
||||
return
|
||||
}
|
||||
if (
|
||||
!RequirementProviderEndpointPolicy.isAllowed(
|
||||
baseUrl = settings.baseUrl,
|
||||
apiKey = settings.apiKey
|
||||
)
|
||||
) {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.UNSAFE_PROVIDER_ENDPOINT)
|
||||
return
|
||||
}
|
||||
if (
|
||||
settings.apiKey.isNotBlank() &&
|
||||
!settingsManager.isSecureCredentialStorageAvailable
|
||||
) {
|
||||
setRequirementFailure(
|
||||
RequirementExtractionFailureCode.SECURE_CREDENTIAL_STORAGE_UNAVAILABLE
|
||||
)
|
||||
return
|
||||
}
|
||||
if (provider.id != ApiProvider.CUSTOM.id && settings.apiKey.isBlank()) {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.PROVIDER_NOT_CONFIGURED)
|
||||
return
|
||||
}
|
||||
|
||||
requirementProbeState.value = RequirementProbeState.RUNNING
|
||||
requirementExtraction.value = null
|
||||
requirementFailureCode.value = null
|
||||
requirementProbeJob = lifecycleScope.launch {
|
||||
try {
|
||||
val fixture = requirementProbeSource.loadFirst().getOrElse {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.SOURCE_UNAVAILABLE)
|
||||
return@launch
|
||||
} ?: run {
|
||||
setRequirementFailure(RequirementExtractionFailureCode.SOURCE_UNAVAILABLE)
|
||||
return@launch
|
||||
}
|
||||
val client = VLMClient(
|
||||
apiKey = settings.apiKey,
|
||||
baseUrl = settings.baseUrl,
|
||||
model = settings.model
|
||||
)
|
||||
val result = RequirementExtractor(
|
||||
gateway = AndroidRequirementVlmGateway(client),
|
||||
providerId = provider.id,
|
||||
model = settings.model
|
||||
).extract(
|
||||
RequirementExtractionInput.from(
|
||||
task = fixture.task,
|
||||
imageBytes = fixture.referenceImageBytes
|
||||
)
|
||||
)
|
||||
when (result) {
|
||||
is RequirementExtractionResult.Completed -> {
|
||||
requirementExtraction.value = result.extraction
|
||||
requirementProbeState.value =
|
||||
if (result.extraction.manualReviewRequired) {
|
||||
RequirementProbeState.MANUAL_REVIEW
|
||||
} else {
|
||||
RequirementProbeState.READY
|
||||
}
|
||||
}
|
||||
is RequirementExtractionResult.Failed -> {
|
||||
setRequirementFailure(result.code)
|
||||
}
|
||||
}
|
||||
} catch (error: CancellationException) {
|
||||
requirementProbeState.value = RequirementProbeState.STOPPED
|
||||
throw error
|
||||
} finally {
|
||||
requirementProbeJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRequirementProbe() {
|
||||
requirementProbeJob?.cancel()
|
||||
}
|
||||
|
||||
private fun setRequirementFailure(code: RequirementExtractionFailureCode) {
|
||||
requirementExtraction.value = null
|
||||
requirementFailureCode.value = code
|
||||
requirementProbeState.value = RequirementProbeState.FAILED
|
||||
}
|
||||
|
||||
private fun checkShizukuPermission(): Boolean {
|
||||
return try {
|
||||
val granted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
Reference in New Issue
Block a user