feat(t214): persist candidate observation identities
This commit is contained in:
@@ -1045,9 +1045,8 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
ExecutionCandidateDraft(
|
||||
ordinal = assessment.ordinal,
|
||||
title =
|
||||
"拼多多图片候选 " +
|
||||
assessment.ordinal,
|
||||
title = candidateEvidence.observedTitle
|
||||
?: "拼多多图片候选 ${assessment.ordinal}",
|
||||
skuText =
|
||||
candidateEvidence.specification
|
||||
.selectedSummary
|
||||
@@ -1059,6 +1058,14 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
price = candidateEvidence.specification
|
||||
.price?.rawText.orEmpty(),
|
||||
cardSignature =
|
||||
candidateEvidence.cardSignature,
|
||||
detailSignature =
|
||||
candidateEvidence.detailSignature,
|
||||
detailEvidenceSha256 =
|
||||
candidateEvidence.detailSha256,
|
||||
specificationEvidenceSha256 =
|
||||
candidateEvidence.specificationSha256,
|
||||
evidenceLocalIDs = emptyList(),
|
||||
evaluation = ExecutionCandidateEvaluation(
|
||||
decision = assessment.decision.name,
|
||||
@@ -1222,11 +1229,17 @@ class MainActivity : ComponentActivity() {
|
||||
candidates = validated.map { candidate ->
|
||||
ExecutionCandidateDraft(
|
||||
ordinal = candidate.ordinal,
|
||||
title = "$taskTitle 候选 ${candidate.ordinal}",
|
||||
title = candidate.observedTitle
|
||||
?: "$taskTitle 候选 ${candidate.ordinal}",
|
||||
skuText = candidate.specification
|
||||
.selectedSummary.orEmpty(),
|
||||
price = candidate.specification
|
||||
.price?.rawText.orEmpty(),
|
||||
cardSignature = candidate.cardSignature,
|
||||
detailSignature = candidate.detailSignature,
|
||||
detailEvidenceSha256 = candidate.detailSha256,
|
||||
specificationEvidenceSha256 =
|
||||
candidate.specificationSha256,
|
||||
evidenceLocalIDs = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -14,6 +14,7 @@ import androidx.annotation.RequiresApi
|
||||
import com.roubao.autopilot.pinduoduo.PinduoduoCandidateCard
|
||||
import com.roubao.autopilot.pinduoduo.PinduoduoCandidateDetailEvidence
|
||||
import com.roubao.autopilot.pinduoduo.PinduoduoEvidenceHash
|
||||
import com.roubao.autopilot.pinduoduo.PinduoduoObservedTitlePolicy
|
||||
import com.roubao.autopilot.readiness.DeviceObservationStore
|
||||
import com.roubao.autopilot.readiness.LoginBlockerDetector
|
||||
import com.roubao.autopilot.readiness.PINDUODUO_PACKAGE
|
||||
@@ -320,7 +321,8 @@ class BuyerAccessibilityService : AccessibilityService() {
|
||||
signature = PinduoduoEvidenceHash.sha256(
|
||||
semanticTexts.joinToString(TEXT_SIGNATURE_SEPARATOR)
|
||||
),
|
||||
semanticTextCount = semanticTexts.size
|
||||
semanticTextCount = semanticTexts.size,
|
||||
observedTitle = PinduoduoObservedTitlePolicy.select(semanticTexts)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -134,7 +134,8 @@ private class CandidateEvidenceStore(context: Context) {
|
||||
detailSemanticTextCount = detail.detail.semanticTextCount,
|
||||
detailAsset = detailAsset,
|
||||
specification = specifications.evidence,
|
||||
specificationAsset = specificationAsset
|
||||
specificationAsset = specificationAsset,
|
||||
observedTitle = detail.detail.observedTitle
|
||||
)
|
||||
evidenceByOrdinal[ordinal] = evidence
|
||||
writeManifest()
|
||||
@@ -172,6 +173,7 @@ private class CandidateEvidenceStore(context: Context) {
|
||||
evidence.cardSemanticTextCount
|
||||
)
|
||||
.put("detail_signature", evidence.detailSignature)
|
||||
.put("observed_title", evidence.observedTitle)
|
||||
.put(
|
||||
"detail_semantic_text_count",
|
||||
evidence.detailSemanticTextCount
|
||||
@@ -208,7 +210,7 @@ private class CandidateEvidenceStore(context: Context) {
|
||||
)
|
||||
}
|
||||
val manifest = JSONObject()
|
||||
.put("schema_version", 3)
|
||||
.put("schema_version", 4)
|
||||
.put("candidate_count", evidenceByOrdinal.size)
|
||||
.put("candidates", candidates)
|
||||
.toString(2)
|
||||
|
||||
+11
@@ -9,6 +9,9 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
data class ValidatedCandidateEvidence(
|
||||
val ordinal: Int,
|
||||
val cardSignature: String,
|
||||
val detailSignature: String,
|
||||
val observedTitle: String?,
|
||||
val detailPngBytes: ByteArray,
|
||||
val detailSha256: String,
|
||||
val specification: PinduoduoSpecificationEvidence,
|
||||
@@ -46,6 +49,11 @@ class CandidateEvidenceSource(
|
||||
?.length
|
||||
?.let { it in 1..160 } != false
|
||||
)
|
||||
require(SHA256_PATTERN.matches(metadata.cardSignature))
|
||||
require(SHA256_PATTERN.matches(metadata.detailSignature))
|
||||
require(
|
||||
metadata.observedTitle?.length?.let { it in 1..160 } != false
|
||||
)
|
||||
require(
|
||||
metadata.specification.price?.let { price ->
|
||||
price.rawText.length in 1..32 &&
|
||||
@@ -69,6 +77,9 @@ class CandidateEvidenceSource(
|
||||
require(totalBytes <= MAX_TOTAL_PNG_BYTES)
|
||||
ValidatedCandidateEvidence(
|
||||
ordinal = metadata.ordinal,
|
||||
cardSignature = metadata.cardSignature,
|
||||
detailSignature = metadata.detailSignature,
|
||||
observedTitle = metadata.observedTitle,
|
||||
detailPngBytes = detail,
|
||||
detailSha256 = metadata.detailAsset.sha256,
|
||||
specification = metadata.specification,
|
||||
|
||||
+30
-2
@@ -14,7 +14,8 @@ data class PinduoduoCandidateCard(
|
||||
|
||||
data class PinduoduoCandidateDetailEvidence(
|
||||
val signature: String,
|
||||
val semanticTextCount: Int
|
||||
val semanticTextCount: Int,
|
||||
val observedTitle: String? = null
|
||||
)
|
||||
|
||||
data class PinduoduoScreenshotCapture(
|
||||
@@ -39,9 +40,36 @@ data class PinduoduoCandidateEvidence(
|
||||
val detailSemanticTextCount: Int,
|
||||
val detailAsset: PinduoduoEvidenceAsset,
|
||||
val specification: PinduoduoSpecificationEvidence,
|
||||
val specificationAsset: PinduoduoEvidenceAsset
|
||||
val specificationAsset: PinduoduoEvidenceAsset,
|
||||
val observedTitle: String? = null
|
||||
)
|
||||
|
||||
object PinduoduoObservedTitlePolicy {
|
||||
private val excludedFragments = listOf(
|
||||
"客服",
|
||||
"收藏",
|
||||
"店铺",
|
||||
"免拼购买",
|
||||
"单独购买",
|
||||
"发起拼单",
|
||||
"确认款式",
|
||||
"已选择",
|
||||
"退货",
|
||||
"包邮"
|
||||
)
|
||||
|
||||
fun select(semanticTexts: List<String>): String? =
|
||||
semanticTexts.asSequence()
|
||||
.map { it.trim().replace(Regex("\\s+"), " ") }
|
||||
.filter { it.length in 6..160 }
|
||||
.filterNot { text ->
|
||||
text.any { it == '¥' || it == '¥' } ||
|
||||
excludedFragments.any(text::contains) ||
|
||||
text.all { it.isDigit() || it in ".,+-/% " }
|
||||
}
|
||||
.maxByOrNull(String::length)
|
||||
}
|
||||
|
||||
object PinduoduoEvidenceHash {
|
||||
fun sha256(value: String): String =
|
||||
sha256(value.toByteArray(Charsets.UTF_8))
|
||||
|
||||
+4
@@ -43,6 +43,10 @@ data class ExecutionCandidateDraft(
|
||||
val price: String = "",
|
||||
val productUrl: String = "",
|
||||
val imageUrl: String = "",
|
||||
val cardSignature: String = "",
|
||||
val detailSignature: String = "",
|
||||
val detailEvidenceSha256: String = "",
|
||||
val specificationEvidenceSha256: String = "",
|
||||
val evidenceLocalIDs: List<String>,
|
||||
val evaluation: ExecutionCandidateEvaluation? = null
|
||||
)
|
||||
|
||||
+19
@@ -404,6 +404,14 @@ class ProcurementRepository(
|
||||
require(batch.candidates.map { it.ordinal } == (1..batch.candidates.size).toList()) {
|
||||
"候选编号必须连续"
|
||||
}
|
||||
require(batch.candidates.all { candidate ->
|
||||
SHA256_PATTERN.matches(candidate.cardSignature) &&
|
||||
SHA256_PATTERN.matches(candidate.detailSignature) &&
|
||||
SHA256_PATTERN.matches(candidate.detailEvidenceSha256) &&
|
||||
SHA256_PATTERN.matches(
|
||||
candidate.specificationEvidenceSha256
|
||||
)
|
||||
}) { "候选持久身份指纹无效" }
|
||||
require(batch.mode == execution.provenance?.mode) { "执行模式不能在回传时变更" }
|
||||
if (batch.mode == ExecutionMode.AI_ASSISTED) {
|
||||
require(batch.provenance != null && batch.provenance.providerId != null) {
|
||||
@@ -883,6 +891,16 @@ class ProcurementRepository(
|
||||
.put("price", candidate.price)
|
||||
.put("product_url", candidate.productUrl)
|
||||
.put("image_url", candidate.imageUrl)
|
||||
.put("card_signature", candidate.cardSignature)
|
||||
.put("detail_signature", candidate.detailSignature)
|
||||
.put(
|
||||
"detail_evidence_sha256",
|
||||
candidate.detailEvidenceSha256
|
||||
)
|
||||
.put(
|
||||
"specification_evidence_sha256",
|
||||
candidate.specificationEvidenceSha256
|
||||
)
|
||||
.put("evidence_asset_ids", JSONArray(candidate.evidenceLocalIDs))
|
||||
.also { json ->
|
||||
candidate.evaluation?.let { evaluation ->
|
||||
@@ -1054,6 +1072,7 @@ class ProcurementRepository(
|
||||
private const val MAX_REFERENCE_DIMENSION = 4_096
|
||||
private const val MAX_REFERENCE_PIXELS = 20_000_000L
|
||||
private const val MAX_OUTBOX_EVIDENCE_BYTES = 8L * 1024L * 1024L
|
||||
private val SHA256_PATTERN = Regex("^[0-9a-f]{64}$")
|
||||
private val COMPLETE_OUTCOMES = setOf(
|
||||
"CANDIDATE_ACCEPTED",
|
||||
"CANDIDATE_REJECTED",
|
||||
|
||||
+4
-3
@@ -150,13 +150,14 @@ class CandidateEvidenceSourceTest {
|
||||
)
|
||||
return PinduoduoCandidateEvidence(
|
||||
ordinal = ordinal,
|
||||
cardSignature = "card-$ordinal",
|
||||
cardSignature = PinduoduoEvidenceHash.sha256("card-$ordinal"),
|
||||
cardSemanticTextCount = 3,
|
||||
detailSignature = "detail-$ordinal",
|
||||
detailSignature = PinduoduoEvidenceHash.sha256("detail-$ordinal"),
|
||||
detailSemanticTextCount = 4,
|
||||
detailAsset = asset,
|
||||
specification = specification(),
|
||||
specificationAsset = asset.copy(fileName = specificationFileName)
|
||||
specificationAsset = asset.copy(fileName = specificationFileName),
|
||||
observedTitle = "候选商品标题 $ordinal"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.roubao.autopilot.pinduoduo
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class PinduoduoObservedTitlePolicyTest {
|
||||
@Test
|
||||
fun `selects longest product-like semantic text`() {
|
||||
val title = PinduoduoObservedTitlePolicy.select(
|
||||
listOf(
|
||||
"¥12.90",
|
||||
"店铺客服",
|
||||
"2026夏季纯棉宽松短袖圆领上衣",
|
||||
"纯棉短袖"
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals("2026夏季纯棉宽松短袖圆领上衣", title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects actions prices and numeric-only text`() {
|
||||
val title = PinduoduoObservedTitlePolicy.select(
|
||||
listOf(
|
||||
"免拼购买",
|
||||
"已选择 黑色 L",
|
||||
"¥12.90",
|
||||
"123456"
|
||||
)
|
||||
)
|
||||
|
||||
assertNull(title)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user