fix(t212): preserve ranked candidate identity

This commit is contained in:
QiuSW
2026-07-27 22:31:17 +08:00
parent ca60cc1edc
commit 4b795a4224
7 changed files with 382 additions and 115 deletions
@@ -93,12 +93,15 @@ import com.roubao.autopilot.procurement.LoginInput
import com.roubao.autopilot.procurement.ExecutionCandidateBatchDraft import com.roubao.autopilot.procurement.ExecutionCandidateBatchDraft
import com.roubao.autopilot.procurement.ExecutionCandidateDraft import com.roubao.autopilot.procurement.ExecutionCandidateDraft
import com.roubao.autopilot.procurement.ExecutionCandidateEvaluation import com.roubao.autopilot.procurement.ExecutionCandidateEvaluation
import com.roubao.autopilot.procurement.ExecutionCandidateIdentityPolicy
import com.roubao.autopilot.procurement.ExecutionCandidateSourceIdentity
import com.roubao.autopilot.procurement.ExecutionHardConstraintEvaluation import com.roubao.autopilot.procurement.ExecutionHardConstraintEvaluation
import com.roubao.autopilot.procurement.ExecutionRecommendation import com.roubao.autopilot.procurement.ExecutionRecommendation
import com.roubao.autopilot.procurement.ExecutionEvidenceDraft import com.roubao.autopilot.procurement.ExecutionEvidenceDraft
import com.roubao.autopilot.procurement.ExecutionMode import com.roubao.autopilot.procurement.ExecutionMode
import com.roubao.autopilot.procurement.ExecutionProvenanceSnapshot import com.roubao.autopilot.procurement.ExecutionProvenanceSnapshot
import com.roubao.autopilot.procurement.ProcurementRepository import com.roubao.autopilot.procurement.ProcurementRepository
import com.roubao.autopilot.procurement.RankedExecutionCandidateDraft
import com.roubao.autopilot.task.RequirementProbeFixture import com.roubao.autopilot.task.RequirementProbeFixture
import com.roubao.autopilot.workflow.WorkflowReport import com.roubao.autopilot.workflow.WorkflowReport
import com.roubao.autopilot.workflow.WorkflowRunner import com.roubao.autopilot.workflow.WorkflowRunner
@@ -144,8 +147,8 @@ class MainActivity : ComponentActivity() {
private val candidateReviewBatch = mutableStateOf<CandidateReviewBatch?>(null) private val candidateReviewBatch = mutableStateOf<CandidateReviewBatch?>(null)
private val candidateEvaluationFailureCode = private val candidateEvaluationFailureCode =
mutableStateOf<CandidateEvaluationFailureCode?>(null) mutableStateOf<CandidateEvaluationFailureCode?>(null)
private val taskCandidateDrafts = private val taskCandidateDrafts =
mutableStateOf<List<ExecutionCandidateDraft>>(emptyList()) mutableStateOf<List<RankedExecutionCandidateDraft>>(emptyList())
private var searchProbeRunner: WorkflowRunner? = null private var searchProbeRunner: WorkflowRunner? = null
private var searchProbeJob: Job? = null private var searchProbeJob: Job? = null
private var requirementProbeJob: Job? = null private var requirementProbeJob: Job? = null
@@ -971,16 +974,19 @@ class MainActivity : ComponentActivity() {
procurementRepository.activeExecutionMode() == procurementRepository.activeExecutionMode() ==
ExecutionMode.AI_ASSISTED ExecutionMode.AI_ASSISTED
) { ) {
val selected = CandidateTopFivePolicy.select( val ranked = CandidateTopFivePolicy.rank(
result.batch result.batch
) )
val evidenceByOrdinal = validated.associateBy { val evidenceByOrdinal = validated.associateBy {
it.ordinal it.ordinal
} }
val drafts = selected.mapIndexed { index, assessment -> val drafts = ranked.map { rankedCandidate ->
val assessment = rankedCandidate.assessment
ExecutionCandidateDraft( ExecutionCandidateDraft(
ordinal = index + 1, ordinal = rankedCandidate.rankedOrdinal,
title = "拼多多图片候选 ${assessment.ordinal}", title =
"拼多多图片候选 " +
rankedCandidate.sourceOrdinal,
skuText = assessment.hardConstraintResults skuText = assessment.hardConstraintResults
.joinToString(" / ") { .joinToString(" / ") {
"${it.kind.name}:${it.expected}" "${it.kind.name}:${it.expected}"
@@ -1008,52 +1014,82 @@ class MainActivity : ComponentActivity() {
) )
) )
} }
val selectedEvidence = selected.mapIndexed { val selectedEvidence = ranked.map { rankedCandidate ->
index,
assessment ->
val candidate = requireNotNull( val candidate = requireNotNull(
evidenceByOrdinal[assessment.ordinal] evidenceByOrdinal[
rankedCandidate.sourceOrdinal
]
)
require(
candidate.sha256 ==
rankedCandidate.evidenceSha256
) )
ExecutionEvidenceDraft( ExecutionEvidenceDraft(
ordinal = index + 1, ordinal = rankedCandidate.rankedOrdinal,
pngBytes = candidate.pngBytes, pngBytes = candidate.pngBytes,
sha256 = candidate.sha256 sha256 = candidate.sha256
) )
} }
taskCandidateDrafts.value = val queued = procurementRepository.queueCandidateBatch(
procurementRepository.queueCandidateBatch( ExecutionCandidateBatchDraft(
ExecutionCandidateBatchDraft( mode = ExecutionMode.AI_ASSISTED,
searchQuery =
candidateSearchKeyword.value
?: PDD_IMAGE_SEARCH_AUDIT_QUERY,
provenance = ExecutionProvenanceSnapshot(
mode = ExecutionMode.AI_ASSISTED, mode = ExecutionMode.AI_ASSISTED,
searchQuery = providerId = result.batch.providerId,
candidateSearchKeyword.value model = result.batch.model,
?: PDD_IMAGE_SEARCH_AUDIT_QUERY, promptVersion =
provenance = ExecutionProvenanceSnapshot( CANDIDATE_EVALUATION_PROMPT_VERSION,
mode = ExecutionMode.AI_ASSISTED, schemaVersion =
providerId = result.batch.providerId, CANDIDATE_EVALUATION_SCHEMA_VERSION,
model = result.batch.model, referenceImageSha256 =
promptVersion = result.batch
CANDIDATE_EVALUATION_PROMPT_VERSION, .requirementReferenceImageSha256
schemaVersion =
CANDIDATE_EVALUATION_SCHEMA_VERSION,
referenceImageSha256 =
result.batch
.requirementReferenceImageSha256
),
candidates = drafts,
recommendation = drafts.firstOrNull()?.let {
ExecutionRecommendation(
candidateOrdinal = 1,
policyVersion =
"sku-hard-constraints-v1",
reasons = listOf(
"SKU颜色和尺码硬约束均确认匹配",
"按评估分和置信度排序"
)
)
}
), ),
selectedEvidence candidates = drafts,
).orEmpty() recommendation = drafts.firstOrNull()?.let {
ExecutionRecommendation(
candidateOrdinal = 1,
policyVersion =
"sku-hard-constraints-v1",
reasons = listOf(
"SKU颜色和尺码硬约束均确认匹配",
"按评估分和置信度排序"
)
)
}
),
selectedEvidence
) ?: run {
setCandidateEvaluationFailure(
CandidateEvaluationFailureCode
.EVIDENCE_INVALID
)
return@launch
}
taskCandidateDrafts.value = runCatching {
ExecutionCandidateIdentityPolicy.bind(
identities = ranked.map { rankedCandidate ->
ExecutionCandidateSourceIdentity(
sourceOrdinal =
rankedCandidate.sourceOrdinal,
rankedOrdinal =
rankedCandidate.rankedOrdinal,
evidenceSha256 =
rankedCandidate.evidenceSha256
)
},
candidates = queued
)
}.getOrElse {
setCandidateEvaluationFailure(
CandidateEvaluationFailureCode
.EVIDENCE_INVALID
)
return@launch
}
} }
candidateReviewBatch.value = result.batch candidateReviewBatch.value = result.batch
candidateEvaluationState.value = when ( candidateEvaluationState.value = when (
@@ -1084,37 +1120,54 @@ class MainActivity : ComponentActivity() {
candidateEvaluationJob?.cancel() candidateEvaluationJob?.cancel()
} }
private suspend fun queueManualProcurementCandidates( private suspend fun queueManualProcurementCandidates(
taskTitle: String, taskTitle: String,
searchQuery: String, searchQuery: String,
evidence: List<PinduoduoCandidateEvidence> evidence: List<PinduoduoCandidateEvidence>
) { ) {
val validated = candidateEvidenceSource.load(evidence).getOrNull() ?: return val validated = candidateEvidenceSource.load(evidence).getOrNull() ?: return
taskCandidateDrafts.value = procurementRepository.queueCandidateBatch( val queued = procurementRepository.queueCandidateBatch(
ExecutionCandidateBatchDraft( ExecutionCandidateBatchDraft(
mode = ExecutionMode.MANUAL_FIRST, mode = ExecutionMode.MANUAL_FIRST,
searchQuery = searchQuery, searchQuery = searchQuery,
provenance = null, provenance = null,
candidates = validated.map { candidate -> candidates = validated.map { candidate ->
ExecutionCandidateDraft( ExecutionCandidateDraft(
ordinal = candidate.ordinal, ordinal = candidate.ordinal,
title = "$taskTitle 候选 ${candidate.ordinal}", title = "$taskTitle 候选 ${candidate.ordinal}",
evidenceLocalIDs = emptyList() evidenceLocalIDs = emptyList()
) )
} }
), ),
validated.map { candidate -> validated.map { candidate ->
ExecutionEvidenceDraft( ExecutionEvidenceDraft(
ordinal = candidate.ordinal, ordinal = candidate.ordinal,
pngBytes = candidate.pngBytes, pngBytes = candidate.pngBytes,
sha256 = candidate.sha256 sha256 = candidate.sha256
) )
} }
).orEmpty() ) ?: return
if (taskCandidateDrafts.value.isNotEmpty()) { taskCandidateDrafts.value = runCatching {
candidateEvaluationState.value = CandidateEvaluationState.MANUAL_REVIEW ExecutionCandidateIdentityPolicy.bind(
} identities = validated.map { candidate ->
} ExecutionCandidateSourceIdentity(
sourceOrdinal = candidate.ordinal,
rankedOrdinal = candidate.ordinal,
evidenceSha256 = candidate.sha256
)
},
candidates = queued
)
}.getOrElse {
setCandidateEvaluationFailure(
CandidateEvaluationFailureCode.EVIDENCE_INVALID
)
return
}
if (taskCandidateDrafts.value.isNotEmpty()) {
candidateEvaluationState.value = CandidateEvaluationState.MANUAL_REVIEW
}
}
private fun manualSearchQuery(title: String, sku: String): String = private fun manualSearchQuery(title: String, sku: String): String =
listOf(title.trim(), sku.trim()) listOf(title.trim(), sku.trim())
@@ -1127,27 +1180,39 @@ class MainActivity : ComponentActivity() {
currentState = candidateEvaluationState.value, currentState = candidateEvaluationState.value,
batch = candidateReviewBatch.value batch = candidateReviewBatch.value
) )
if (candidateEvaluationState.value != CandidateEvaluationState.HUMAN_ACCEPTED && if (
taskCandidateDrafts.value.isNotEmpty() candidateEvaluationState.value != CandidateEvaluationState.HUMAN_ACCEPTED &&
) { taskCandidateDrafts.value.isNotEmpty()
candidateEvaluationState.value = CandidateEvaluationState.HUMAN_ACCEPTED ) {
} candidateEvaluationState.value = CandidateEvaluationState.HUMAN_ACCEPTED
if (candidateEvaluationState.value == CandidateEvaluationState.HUMAN_ACCEPTED) { }
val recommendedOrdinal = candidateReviewBatch.value if (
?.recommendedCandidateOrdinal candidateEvaluationState.value ==
val candidate = taskCandidateDrafts.value.firstOrNull { CandidateEvaluationState.HUMAN_ACCEPTED
it.ordinal == recommendedOrdinal ) {
} ?: taskCandidateDrafts.value.firstOrNull() val candidate = runCatching {
if (candidate != null && procurementRepository.currentProbeTask() != null) { ExecutionCandidateIdentityPolicy.recommended(
lifecycleScope.launch { taskCandidateDrafts.value
procurementRepository.completeExecution( )
outcome = "CANDIDATE_ACCEPTED", }.getOrElse {
operatorReason = operatorReason, setCandidateEvaluationFailure(
candidate = candidate CandidateEvaluationFailureCode.EVIDENCE_INVALID
) )
} return
} }
} if (
candidate != null &&
procurementRepository.currentProbeTask() != null
) {
lifecycleScope.launch {
procurementRepository.completeExecution(
outcome = "CANDIDATE_ACCEPTED",
operatorReason = operatorReason,
candidate = candidate
)
}
}
}
} }
private fun rejectCandidateReview(operatorReason: String) { private fun rejectCandidateReview(operatorReason: String) {
@@ -1183,6 +1248,7 @@ class MainActivity : ComponentActivity() {
candidateReviewBatch.value = null candidateReviewBatch.value = null
candidateEvaluationFailureCode.value = null candidateEvaluationFailureCode.value = null
candidateEvaluationState.value = CandidateEvaluationState.IDLE candidateEvaluationState.value = CandidateEvaluationState.IDLE
taskCandidateDrafts.value = emptyList()
} }
private fun checkShizukuPermission(): Boolean { private fun checkShizukuPermission(): Boolean {
@@ -19,6 +19,56 @@ data class ExecutionCandidateDraft(
val evaluation: ExecutionCandidateEvaluation? = null val evaluation: ExecutionCandidateEvaluation? = null
) )
data class ExecutionCandidateSourceIdentity(
val sourceOrdinal: Int,
val rankedOrdinal: Int,
val evidenceSha256: String
)
data class RankedExecutionCandidateDraft(
val identity: ExecutionCandidateSourceIdentity,
val candidate: ExecutionCandidateDraft
)
object ExecutionCandidateIdentityPolicy {
private val sha256Pattern = Regex("^[0-9a-f]{64}$")
fun bind(
identities: List<ExecutionCandidateSourceIdentity>,
candidates: List<ExecutionCandidateDraft>
): List<RankedExecutionCandidateDraft> {
require(identities.size == candidates.size)
require(
identities.map { it.rankedOrdinal } ==
(1..identities.size).toList()
)
require(identities.all { it.sourceOrdinal > 0 })
require(identities.map { it.sourceOrdinal }.distinct().size == identities.size)
require(identities.all { sha256Pattern.matches(it.evidenceSha256) })
val candidatesByOrdinal = candidates.associateBy { it.ordinal }
require(candidatesByOrdinal.size == candidates.size)
return identities.map { identity ->
RankedExecutionCandidateDraft(
identity = identity,
candidate = requireNotNull(
candidatesByOrdinal[identity.rankedOrdinal]
)
)
}
}
fun recommended(
candidates: List<RankedExecutionCandidateDraft>
): ExecutionCandidateDraft? =
when {
candidates.isEmpty() -> null
else -> candidates.single {
it.identity.rankedOrdinal == 1 &&
it.candidate.ordinal == 1
}.candidate
}
}
data class ExecutionCandidateEvaluation( data class ExecutionCandidateEvaluation(
val decision: String, val decision: String,
val score: Double, val score: Double,
@@ -59,6 +59,19 @@ data class CandidateAssessment(
val hardConstraintResults: List<CandidateHardConstraintResult> = emptyList() val hardConstraintResults: List<CandidateHardConstraintResult> = emptyList()
) )
data class RankedCandidateAssessment(
val sourceOrdinal: Int,
val rankedOrdinal: Int,
val evidenceSha256: String,
val assessment: CandidateAssessment
) {
init {
require(sourceOrdinal == assessment.ordinal)
require(rankedOrdinal > 0)
require(evidenceSha256 == assessment.evidenceSha256)
}
}
enum class CandidateBatchConclusion { enum class CandidateBatchConclusion {
SUGGESTED, SUGGESTED,
NO_MATCH, NO_MATCH,
@@ -166,10 +179,10 @@ object CandidateHumanReviewPolicy {
} }
object CandidateTopFivePolicy { object CandidateTopFivePolicy {
fun select( fun rank(
batch: CandidateReviewBatch, batch: CandidateReviewBatch,
limit: Int = 5 limit: Int = 5
): List<CandidateAssessment> { ): List<RankedCandidateAssessment> {
require(limit in 1..5) require(limit in 1..5)
return batch.assessments return batch.assessments
.filter { assessment -> .filter { assessment ->
@@ -188,5 +201,19 @@ object CandidateTopFivePolicy {
.thenBy { it.ordinal } .thenBy { it.ordinal }
) )
.take(limit) .take(limit)
.mapIndexed { index, assessment ->
RankedCandidateAssessment(
sourceOrdinal = assessment.ordinal,
rankedOrdinal = index + 1,
evidenceSha256 = assessment.evidenceSha256,
assessment = assessment
)
}
} }
fun select(
batch: CandidateReviewBatch,
limit: Int = 5
): List<CandidateAssessment> =
rank(batch, limit).map { it.assessment }
} }
@@ -0,0 +1,87 @@
package com.roubao.autopilot.procurement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertThrows
import org.junit.Test
class ExecutionCandidateIdentityPolicyTest {
@Test
fun `recommended candidate uses ranked first instead of source ordinal`() {
val bindings = ExecutionCandidateIdentityPolicy.bind(
identities = listOf(
identity(sourceOrdinal = 3, rankedOrdinal = 1),
identity(sourceOrdinal = 1, rankedOrdinal = 2),
identity(sourceOrdinal = 2, rankedOrdinal = 3)
),
candidates = listOf(candidate(1), candidate(2), candidate(3))
)
val recommended = ExecutionCandidateIdentityPolicy.recommended(bindings)
assertEquals(1, recommended?.ordinal)
assertEquals(3, bindings.first().identity.sourceOrdinal)
assertEquals("evidence-1", recommended?.evidenceLocalIDs?.single())
}
@Test
fun `filtered source ordinals keep continuous ranked identities`() {
val bindings = ExecutionCandidateIdentityPolicy.bind(
identities = listOf(
identity(sourceOrdinal = 2, rankedOrdinal = 1),
identity(sourceOrdinal = 5, rankedOrdinal = 2)
),
candidates = listOf(candidate(1), candidate(2))
)
assertEquals(
listOf(2, 5),
bindings.map { it.identity.sourceOrdinal }
)
assertEquals(
listOf(1, 2),
bindings.map { it.candidate.ordinal }
)
}
@Test
fun `empty candidates have no recommendation`() {
assertNull(
ExecutionCandidateIdentityPolicy.recommended(
ExecutionCandidateIdentityPolicy.bind(
identities = emptyList(),
candidates = emptyList()
)
)
)
}
@Test
fun `missing ranked ordinal is rejected instead of falling back`() {
assertThrows(IllegalArgumentException::class.java) {
ExecutionCandidateIdentityPolicy.bind(
identities = listOf(
identity(sourceOrdinal = 3, rankedOrdinal = 1)
),
candidates = listOf(candidate(2))
)
}
}
private fun identity(
sourceOrdinal: Int,
rankedOrdinal: Int
): ExecutionCandidateSourceIdentity =
ExecutionCandidateSourceIdentity(
sourceOrdinal = sourceOrdinal,
rankedOrdinal = rankedOrdinal,
evidenceSha256 = sourceOrdinal.toString(16).padStart(64, '0')
)
private fun candidate(ordinal: Int): ExecutionCandidateDraft =
ExecutionCandidateDraft(
ordinal = ordinal,
title = "candidate-$ordinal",
evidenceLocalIDs = listOf("evidence-$ordinal")
)
}
@@ -6,7 +6,7 @@ import org.junit.Test
class CandidateTopFivePolicyTest { class CandidateTopFivePolicyTest {
@Test @Test
fun `sorts eligible matches and limits result to five`() { fun `sorts eligible matches and limits result to five`() {
val selected = CandidateTopFivePolicy.select( val ranked = CandidateTopFivePolicy.rank(
batch( batch(
assessment(1, score = 0.80, confidence = 0.90), assessment(1, score = 0.80, confidence = 0.90),
assessment(2, score = 0.90, confidence = 0.80), assessment(2, score = 0.90, confidence = 0.80),
@@ -24,7 +24,18 @@ class CandidateTopFivePolicyTest {
) )
) )
assertEquals(listOf(3, 2, 5, 6, 7), selected.map { it.ordinal }) assertEquals(
listOf(3, 2, 5, 6, 7),
ranked.map { it.sourceOrdinal }
)
assertEquals(
listOf(1, 2, 3, 4, 5),
ranked.map { it.rankedOrdinal }
)
assertEquals(
ranked.map { it.assessment.evidenceSha256 },
ranked.map { it.evidenceSha256 }
)
} }
@Test @Test
@@ -45,6 +56,24 @@ class CandidateTopFivePolicyTest {
assertEquals(listOf(1), selected.map { it.ordinal }) assertEquals(listOf(1), selected.map { it.ordinal })
} }
@Test
fun `original third candidate becomes stable first ranked candidate`() {
val ranked = CandidateTopFivePolicy.rank(
batch(
assessment(1, score = 0.80, confidence = 0.90),
assessment(2, score = 0.85, confidence = 0.90),
assessment(3, score = 0.95, confidence = 0.95)
)
)
assertEquals(3, ranked.first().sourceOrdinal)
assertEquals(1, ranked.first().rankedOrdinal)
assertEquals(
3.toString(16).padStart(64, '0'),
ranked.first().evidenceSha256
)
}
private fun batch(vararg assessments: CandidateAssessment): CandidateReviewBatch = private fun batch(vararg assessments: CandidateAssessment): CandidateReviewBatch =
CandidateReviewBatch( CandidateReviewBatch(
assessments = assessments.toList(), assessments = assessments.toList(),
+9 -9
View File
@@ -5,10 +5,9 @@
## 当前快照 ## 当前快照
- 日期:2026-07-27 - 日期:2026-07-27
- 阶段:T-211 参考图召回与 SKU 硬匹配完成;下一步 T-212 修复候选重排映射, - 阶段:T-212 候选重排身份映射完成;下一步 T-213 只读核验拼多多规格弹窗
随后 T-213 只读核验拼多多规格弹窗
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、 - Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、
T-210、T-211 均已纳入 Git 历史 T-210、T-211、T-212 均已纳入 Git 历史
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码 - 生产代码:`android-buyer/` 已接入 Roubao Android 源码
- Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留 - Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留
@@ -18,8 +17,8 @@
- 本机 Android 工具:JDK 17.0.13、Command-line Tools 22.0、SDK 34、 - 本机 Android 工具:JDK 17.0.13、Command-line Tools 22.0、SDK 34、
Build Tools 34.0.0、Platform Tools/ADB 37.0.0;用户级 SDK 环境变量已设置 Build Tools 34.0.0、Platform Tools/ADB 37.0.0;用户级 SDK 环境变量已设置
- Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建 - Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建
- 测试:T-211 运行 `:app:testDebugUnitTest` 和 `:app:assembleDebug` 通过,20 个 suite - 测试:T-212 运行 `:app:testDebugUnitTest` 和 `:app:assembleDebug` 通过,21 个 suite
共 108 个测试、0 失败;最新 Debug APK 已安装到 PKG110 共 113 个测试、0 失败;T-211 Debug APK 已安装到 PKG110
- 后端测试:T-211 运行 `GOTOOLCHAIN=local go test -count=1 ./...` 和 - 后端测试:T-211 运行 `GOTOOLCHAIN=local go test -count=1 ./...` 和
`go vet ./...` 通过;T-207 的全包 race 与 migration 验证继续有效 `go vet ./...` 通过;T-207 的全包 race 与 migration 验证继续有效
- 原型:4 个管理 Web 页面和 7 个 Android 页面均可离线独立打开;Playwright - 原型:4 个管理 Web 页面和 7 个 Android 页面均可离线独立打开;Playwright
@@ -108,7 +107,7 @@
| `docs/tasks/T-209.md` | DONE | 登录失败保留采购员和设备输入以便直接重试 | | `docs/tasks/T-209.md` | DONE | 登录失败保留采购员和设备输入以便直接重试 |
| `docs/tasks/T-210.md` | DONE | 兼容拼多多 8.17 搜索结果页与长词省略显示 | | `docs/tasks/T-210.md` | DONE | 兼容拼多多 8.17 搜索结果页与长词省略显示 |
| `docs/tasks/T-211.md` | DONE | 参考图召回、SKU 颜色尺码硬匹配与 0..5 候选回传 | | `docs/tasks/T-211.md` | DONE | 参考图召回、SKU 颜色尺码硬匹配与 0..5 候选回传 |
| `docs/tasks/T-212.md` | TODO | 修复重排候选、推荐、证据与人工接受的身份映射 | | `docs/tasks/T-212.md` | DONE | 修复重排候选、推荐、证据与人工接受的身份映射 |
| `docs/tasks/T-213.md` | TODO | 只读采集拼多多规格弹窗中的颜色和尺码证据 | | `docs/tasks/T-213.md` | TODO | 只读采集拼多多规格弹窗中的颜色和尺码证据 |
| `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 | | `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 |
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 | | `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
@@ -120,9 +119,10 @@
## 任务摘要 ## 任务摘要
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、T-210、T-211。 - 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、T-210、T-211、
- 正在进行:T-212 修复 Top 5 候选 ordinal 与证据映射。 T-212。
- 下一个可领取任务:无;T-213 依赖 T-212 完成。 - 正在进行:无。
- 下一个可领取任务:T-213 拼多多规格弹窗只读核验。
- 后置任务:T-213 拼多多规格弹窗只读核验;完成后再做 T-208 候选决策数据与人工 - 后置任务:T-213 拼多多规格弹窗只读核验;完成后再做 T-208 候选决策数据与人工
理由闭环。 理由闭环。
+15 -7
View File
@@ -4,7 +4,7 @@ title: 修复 Top 5 候选 ordinal 与证据映射
phase: 2 phase: 2
deps: deps:
- T-211 - T-211
status: DOING status: DONE
created: 2026-07-27 created: 2026-07-27
context_ref: edfee2c context_ref: edfee2c
work_branch: null work_branch: null
@@ -44,12 +44,12 @@ VLM 原始候选 ordinal 查找已经重排的 `ExecutionCandidateDraft`。当
## 验收要点 ## 验收要点
- [ ] 原始候选 3 得分最高时,回传候选 1、推荐项和人工接受终态都引用原候选 3 的证据。 - [x] 原始候选 3 得分最高时,回传候选 1、推荐项和人工接受终态都引用原候选 3 的证据。
- [ ] 过滤后只剩原始候选 2、5 时,回传 ordinal 为 1、2,证据分别保持 2、5 的来源。 - [x] 过滤后只剩原始候选 2、5 时,回传 ordinal 为 1、2,证据分别保持 2、5 的来源。
- [ ] 相同分数按置信度和原始曝光顺序稳定排序,重试不会改变映射。 - [x] 相同分数按置信度和原始曝光顺序稳定排序,重试不会改变映射。
- [ ] 空候选、单候选、拒绝和无匹配路径不产生越界或错误 fallback。 - [x] 空候选、单候选、拒绝和无匹配路径不产生越界或错误 fallback。
- [ ] 后端 schema v2 的推荐首项、连续 ordinal 和证据资产校验继续通过。 - [x] 后端 schema v2 的推荐首项、连续 ordinal 和证据资产校验继续通过。
- [ ] Android 单元测试和 Debug 构建通过;人工接受仍保持 - [x] Android 单元测试和 Debug 构建通过;人工接受仍保持
`order_submitted=false`。 `order_submitted=false`。
## 边界 ## 边界
@@ -64,3 +64,11 @@ VLM 原始候选 ordinal 查找已经重排的 `ExecutionCandidateDraft`。当
采集,避免把错误候选写入后续优化数据。 采集,避免把错误候选写入后续优化数据。
- 2026-07-27:基于 `1d9dd08` 领取任务,先建立可单测的显式映射模型,再接入候选 - 2026-07-27:基于 `1d9dd08` 领取任务,先建立可单测的显式映射模型,再接入候选
批次和人工接受路径。 批次和人工接受路径。
- 2026-07-27:新增 `RankedCandidateAssessment` 和
`ExecutionCandidateSourceIdentity`,显式保存原始 ordinal、重排 ordinal 和证据
SHA-256。候选批次、推荐和人工接受均从同一映射生成;删除按原始 ordinal 查找和
“找不到就取第一个”的 fallback,映射不一致时进入受控证据失败。
- 2026-07-27:新增原候选 3 重排为 1、过滤后原候选 2/5 重排为 1/2、空候选和
缺失 ordinal 回归测试。`.\gradlew.bat :app:testDebugUnitTest :app:assembleDebug
--no-daemon` 通过,21 个 suite 共 113 个测试、0 失败;
`GOTOOLCHAIN=local go test -count=1 ./...` 通过。