feat(cleaner): add automatic retry mechanism for failed orders

- Add retry logic with max 2 attempts per failed order
- Track retry statistics (retriedOrders, successfulRetries)
- Generate detailed retry report section in execution reports
- Display retry metrics in ExecutionReportDialog UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-03-13 15:59:10 +08:00
parent a25ffd75c5
commit 715dfb4d71
9 changed files with 325 additions and 21 deletions

View File

@@ -7,6 +7,12 @@ import { createLogger } from '../logger'
const log = createLogger('CleanerService')
interface RetryResult {
retriedOrders: number
successfulRetries: number
updatedDetails: OrderCleanDetail[]
}
/**
* Cleaner Service Options
*/
@@ -102,7 +108,9 @@ export class CleanerService {
materialsDeleted: 0,
materialsSkipped: 0,
errors: [],
details: []
details: [],
retriedOrders: 0,
successfulRetries: 0
}
const totalOrders = input.orderNumbers.length
@@ -158,7 +166,11 @@ export class CleanerService {
materialsDeleted: 0,
materialsSkipped: 0,
errors: [message],
skippedMaterials: []
skippedMaterials: [],
retryCount: 0,
retryAttempts: [],
retriedAt: undefined,
retrySuccess: false
})
}
}
@@ -171,6 +183,36 @@ export class CleanerService {
materialsSkipped: result.materialsSkipped,
errorCount: result.errors.length
})
// Retry failed orders
const retryResult = await this.retryFailedOrders({
workFrame,
popupPage,
failedDetails: result.details.filter((d) => d.errors.length > 0),
deleteSet,
dryRun,
onProgress: input.onProgress
})
// Merge retry results
result.retriedOrders = retryResult.retriedOrders
result.successfulRetries = retryResult.successfulRetries
// Update details with retry information
retryResult.updatedDetails.forEach((updatedDetail) => {
const index = result.details.findIndex((d) => d.orderNumber === updatedDetail.orderNumber)
if (index !== -1) {
result.details[index] = updatedDetail
}
})
// Clear errors for successfully retried orders
const successfulRetryOrders = new Set(
retryResult.updatedDetails.filter((d) => d.retrySuccess).map((d) => d.orderNumber)
)
result.errors = result.errors.filter(
(err) => !successfulRetryOrders.has(err.split(':')[0].replace('Order ', ''))
)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Cleaner failed', { error: message })
@@ -265,7 +307,11 @@ export class CleanerService {
materialsDeleted: 0,
materialsSkipped: 0,
errors: [],
skippedMaterials: []
skippedMaterials: [],
retryCount: 0,
retryAttempts: [],
retriedAt: undefined,
retrySuccess: false
}
// Query the order (Python lines 187-189)
@@ -531,4 +577,121 @@ export class CleanerService {
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Retry failed orders from the initial execution
* Maximum 2 retry attempts per failed order
*/
private async retryFailedOrders(params: {
workFrame: FrameLocator
popupPage: Page
failedDetails: OrderCleanDetail[]
deleteSet: Set<string>
dryRun: boolean
onProgress?: (
message: string,
progress?: number,
extra?: Partial<import('../../types/cleaner.types').CleanerProgress>
) => void
}): Promise<RetryResult> {
const { workFrame, popupPage, failedDetails, deleteSet, dryRun, onProgress } = params
const result: RetryResult = {
retriedOrders: 0,
successfulRetries: 0,
updatedDetails: []
}
if (failedDetails.length === 0) {
return result
}
log.info('Starting retry for failed orders', { count: failedDetails.length })
const MAX_RETRIES = 2
for (const failedDetail of failedDetails) {
const orderNumber = failedDetail.orderNumber
const retryAttempts: import('../../types/cleaner.types').RetryAttempt[] = []
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
log.info(`Retrying order ${orderNumber} (attempt ${attempt}/${MAX_RETRIES})`)
// Create a new detail for retry
const retryDetail: OrderCleanDetail = {
orderNumber,
materialsDeleted: 0,
materialsSkipped: 0,
errors: [],
skippedMaterials: [],
retryCount: attempt,
retryAttempts: [],
retriedAt: undefined,
retrySuccess: false
}
// Re-run the order processing
await this.processOrder({
workFrame,
popupPage,
orderNumber,
orderIndex: 0, // Not used for retry
totalOrders: failedDetails.length,
deleteSet,
dryRun,
onProgress: (message, progress, extra) => {
onProgress?.(
`[重试 ${attempt}/${MAX_RETRIES}] ${message}`,
progress,
extra ? { ...extra, phase: 'processing' as const } : undefined
)
}
})
// If we reach here, retry succeeded
result.successfulRetries++
log.info(`Retry succeeded for order ${orderNumber}`)
// Merge the successful retry detail
result.updatedDetails.push({
...retryDetail,
retriedAt: Date.now(),
retrySuccess: true
})
result.retriedOrders++
break // Exit retry loop for this order
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.warn(`Retry attempt ${attempt} failed for order ${orderNumber}: ${message}`)
retryAttempts.push({
attempt,
error: message,
timestamp: Date.now()
})
if (attempt === MAX_RETRIES) {
// All retries exhausted
log.error(`All retries failed for order ${orderNumber}`)
result.updatedDetails.push({
...failedDetail,
retryCount: MAX_RETRIES,
retryAttempts,
retriedAt: Date.now(),
retrySuccess: false
})
result.retriedOrders++
}
}
}
}
log.info('Retry process completed', {
retriedOrders: result.retriedOrders,
successfulRetries: result.successfulRetries
})
return result
}
}