diag: 总页数未更新问题 — 增加Reconciliation诊断日志 + 修复keepCurrentWindow后pending不移除

1. RDEPUBPageMapReconciliationCoordinator: 在evaluateTakeover、
   evaluateSegmentTakeover、evaluateFullPageMapTakeover的每个返回点
   增加RDEPUBBackgroundTrace日志,记录关键决策参数:
   - currentSpineIndex、candidateChapters/pages、currentChapters/pages
   - hasPrev/hasNext、lastBuildableSpineIndex
   - jumpSession protected coverage ratio
   用于确认fullMap被拒绝的具体原因

2. RDEPUBPresentationRuntime: .keepCurrentWindow分支增加
   removePendingPageMapUpdate(at: index),防止被拒绝的
   reconcileFullMap update无限期留在pending队列中不被消费。
   之前return false不移除,导致:
   - 后续commitPendingPageMapUpdateIfNeeded重试时条件不变
     仍然keepCurrentWindow,形成无效循环
   - 如果用户不再翻页,总页数永远不更新

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shenlei 2026-06-25 17:33:16 +08:00
parent c555329948
commit 2fa58b1026
2 changed files with 34 additions and 5 deletions

View File

@ -28,8 +28,10 @@ final class RDEPUBPageMapReconciliationCoordinator {
guard let currentWindow else { guard let currentWindow else {
if let candidatePageMap { if let candidatePageMap {
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no currentWindow, fullReplace candidateChapters=\(candidatePageMap.totalChapters) candidatePages=\(candidatePageMap.totalPages)")
return .fullReplace(candidatePageMap) return .fullReplace(candidatePageMap)
} }
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no currentWindow, no candidate")
return .keepCurrentWindow return .keepCurrentWindow
} }
@ -54,7 +56,7 @@ final class RDEPUBPageMapReconciliationCoordinator {
if coverageRatio < 0.8 { if coverageRatio < 0.8 {
RDEPUBBackgroundTrace.log( RDEPUBBackgroundTrace.log(
"Reconciliation", "Reconciliation",
"blocked: candidate doesn't cover protected area (coverage=\(coverageRatio))" "evaluateTakeover: blocked by jumpSession protection coverage=\(String(format: "%.2f", coverageRatio)) currentSpine=\(currentSpineIndex) protected=\(protectedIndices)"
) )
return .keepCurrentWindow return .keepCurrentWindow
} }
@ -72,7 +74,7 @@ final class RDEPUBPageMapReconciliationCoordinator {
if !hasPrev || !hasNext { if !hasPrev || !hasNext {
RDEPUBBackgroundTrace.log( RDEPUBBackgroundTrace.log(
"Reconciliation", "Reconciliation",
"blocked: missing adjacent chapter coverage" "evaluateTakeover: blocked by missing adjacent coverage (segment) currentSpine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext)"
) )
return .keepCurrentWindow return .keepCurrentWindow
} }
@ -85,7 +87,7 @@ final class RDEPUBPageMapReconciliationCoordinator {
if candidateSegment.renderSignature != currentRenderSignature { if candidateSegment.renderSignature != currentRenderSignature {
RDEPUBBackgroundTrace.log( RDEPUBBackgroundTrace.log(
"Reconciliation", "Reconciliation",
"blocked: render signature mismatch" "evaluateTakeover: blocked by render signature mismatch"
) )
return .keepCurrentWindow return .keepCurrentWindow
} }
@ -109,6 +111,7 @@ final class RDEPUBPageMapReconciliationCoordinator {
) )
} }
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no candidate, keepCurrentWindow")
return .keepCurrentWindow return .keepCurrentWindow
} }
@ -123,6 +126,10 @@ final class RDEPUBPageMapReconciliationCoordinator {
if let currentSpineIndex { if let currentSpineIndex {
if !candidateIndices.contains(currentSpineIndex) { if !candidateIndices.contains(currentSpineIndex) {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateSegmentTakeover: keepCurrentWindow — currentSpine=\(currentSpineIndex) not in candidate"
)
return .keepCurrentWindow return .keepCurrentWindow
} }
} }
@ -132,6 +139,10 @@ final class RDEPUBPageMapReconciliationCoordinator {
let hasNext = candidateIndices.contains(currentSpineIndex + 1) || let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
currentSpineIndex == lastBuildableSpineIndex currentSpineIndex == lastBuildableSpineIndex
if !hasPrev || !hasNext { if !hasPrev || !hasNext {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateSegmentTakeover: keepCurrentWindow — missing adjacent spine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext)"
)
return .keepCurrentWindow return .keepCurrentWindow
} }
} }
@ -164,9 +175,14 @@ final class RDEPUBPageMapReconciliationCoordinator {
lastBuildableSpineIndex: Int lastBuildableSpineIndex: Int
) -> RDEPUBPageMapTakeoverDecision { ) -> RDEPUBPageMapTakeoverDecision {
let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex }) let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex })
let currentEntries = currentWindow.entries.count
if let currentSpineIndex { if let currentSpineIndex {
if !candidateIndices.contains(currentSpineIndex) { if !candidateIndices.contains(currentSpineIndex) {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateFullPageMapTakeover: keepCurrentWindow — currentSpine=\(currentSpineIndex) not in candidate (candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries) candidatePages=\(candidatePageMap.totalPages) currentPages=\(currentWindow.totalPages))"
)
return .keepCurrentWindow return .keepCurrentWindow
} }
} }
@ -176,15 +192,27 @@ final class RDEPUBPageMapReconciliationCoordinator {
let hasNext = candidateIndices.contains(currentSpineIndex + 1) || let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
currentSpineIndex == lastBuildableSpineIndex currentSpineIndex == lastBuildableSpineIndex
if !hasPrev || !hasNext { if !hasPrev || !hasNext {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateFullPageMapTakeover: keepCurrentWindow — missing adjacent spine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext) lastBuildable=\(lastBuildableSpineIndex)"
)
return .keepCurrentWindow return .keepCurrentWindow
} }
} }
let isComplete = candidateIndices.count >= currentWindow.entries.count let isComplete = candidateIndices.count >= currentEntries
if isComplete { if isComplete {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateFullPageMapTakeover: fullReplace — candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries) candidatePages=\(candidatePageMap.totalPages) currentPages=\(currentWindow.totalPages)"
)
return .fullReplace(candidatePageMap) return .fullReplace(candidatePageMap)
} }
RDEPUBBackgroundTrace.log(
"Reconciliation",
"evaluateFullPageMapTakeover: keepCurrentWindow — candidateChapters=\(candidateIndices.count) < currentChapters=\(currentEntries)"
)
return .keepCurrentWindow return .keepCurrentWindow
} }

View File

@ -226,7 +226,8 @@ final class RDEPUBPresentationRuntime {
switch decision { switch decision {
case .keepCurrentWindow: case .keepCurrentWindow:
RDEPUBBackgroundTrace.log("Reconciliation", "decision: keepCurrentWindow") RDEPUBBackgroundTrace.log("Reconciliation", "decision: keepCurrentWindow — pending update removed to prevent indefinite retry")
removePendingPageMapUpdate(at: index)
return false return false
case .fullReplace(let newPageMap): case .fullReplace(let newPageMap):