feat(cleaner): organize videos by batch ID and rename by order number
- Video directory now includes batchId: logs/video/{batchId}/
- Each order's video is renamed to {orderNumber}.webm after the
detail page is closed
- Handles name collisions (retries) by appending timestamp
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@ export class CleanerApplicationService {
|
|||||||
return emptyResult
|
return emptyResult
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoDir = input.recordVideo ? this.ensureVideoDir() : undefined
|
const videoDir = input.recordVideo ? this.ensureVideoDir(batchId) : undefined
|
||||||
|
|
||||||
authService = new ErpAuthService({
|
authService = new ErpAuthService({
|
||||||
url: erpConfig.url,
|
url: erpConfig.url,
|
||||||
@@ -178,7 +178,7 @@ export class CleanerApplicationService {
|
|||||||
processConcurrency: input.processConcurrency ?? 1
|
processConcurrency: input.processConcurrency ?? 1
|
||||||
})
|
})
|
||||||
|
|
||||||
let cleaner = new CleanerService(authService)
|
let cleaner = new CleanerService(authService, { videoDir })
|
||||||
let result = await cleaner.clean(modifiedInput)
|
let result = await cleaner.clean(modifiedInput)
|
||||||
|
|
||||||
// Outer retry: re-login and re-run all orders on fatal crash
|
// Outer retry: re-login and re-run all orders on fatal crash
|
||||||
@@ -246,7 +246,7 @@ export class CleanerApplicationService {
|
|||||||
await historyDao.insertOrderRecords(batchId, 2, orderInputs)
|
await historyDao.insertOrderRecords(batchId, 2, orderInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleaner = new CleanerService(authService)
|
cleaner = new CleanerService(authService, { videoDir })
|
||||||
result = await cleaner.clean(modifiedInput)
|
result = await cleaner.clean(modifiedInput)
|
||||||
|
|
||||||
// Save attempt 2 result
|
// Save attempt 2 result
|
||||||
@@ -342,8 +342,8 @@ export class CleanerApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureVideoDir(): string | undefined {
|
private ensureVideoDir(batchId: string): string {
|
||||||
const videoDir = path.join(getLogDir(), 'video')
|
const videoDir = path.join(getLogDir(), 'video', batchId)
|
||||||
if (!fs.existsSync(videoDir)) {
|
if (!fs.existsSync(videoDir)) {
|
||||||
fs.mkdirSync(videoDir, { recursive: true })
|
fs.mkdirSync(videoDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
} from '../../types/cleaner.types'
|
} from '../../types/cleaner.types'
|
||||||
import type { ErpSession } from '../../types/erp.types'
|
import type { ErpSession } from '../../types/erp.types'
|
||||||
import type { FrameLocator, Locator, Page } from 'playwright'
|
import type { FrameLocator, Locator, Page } from 'playwright'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
import { createLogger, run, trackDuration } from '../logger'
|
import { createLogger, run, trackDuration } from '../logger'
|
||||||
import { capturePageContext } from './erp-error-context'
|
import { capturePageContext } from './erp-error-context'
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ class ConcurrencyTracker {
|
|||||||
export interface CleanerOptions {
|
export interface CleanerOptions {
|
||||||
dryRun?: boolean
|
dryRun?: boolean
|
||||||
verbose?: boolean
|
verbose?: boolean
|
||||||
|
videoDir?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,10 +179,12 @@ export async function runWithConcurrency<T, R>(
|
|||||||
export class CleanerService {
|
export class CleanerService {
|
||||||
private authService: ErpAuthService
|
private authService: ErpAuthService
|
||||||
private dryRun: boolean
|
private dryRun: boolean
|
||||||
|
private videoDir: string | undefined
|
||||||
|
|
||||||
constructor(authService: ErpAuthService, options: CleanerOptions = {}) {
|
constructor(authService: ErpAuthService, options: CleanerOptions = {}) {
|
||||||
this.authService = authService
|
this.authService = authService
|
||||||
this.dryRun = options.dryRun ?? false
|
this.dryRun = options.dryRun ?? false
|
||||||
|
this.videoDir = options.videoDir
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1275,6 +1280,31 @@ export class CleanerService {
|
|||||||
log.debug('[详情页面清理] 准备关闭详情页面', { pageUrl: detailPage.url() })
|
log.debug('[详情页面清理] 准备关闭详情页面', { pageUrl: detailPage.url() })
|
||||||
await detailPage.close()
|
await detailPage.close()
|
||||||
log.debug('[详情页面清理完成] 详情页已关闭')
|
log.debug('[详情页面清理完成] 详情页已关闭')
|
||||||
|
|
||||||
|
// Rename video file to order number if video recording is enabled
|
||||||
|
if (this.videoDir && expectedOrderNumber) {
|
||||||
|
try {
|
||||||
|
const video = detailPage.video()
|
||||||
|
if (video) {
|
||||||
|
const originalPath = await video.path()
|
||||||
|
if (originalPath && fs.existsSync(originalPath)) {
|
||||||
|
const ext = path.extname(originalPath) || '.webm'
|
||||||
|
const orderVideoPath = path.join(this.videoDir, `${expectedOrderNumber}${ext}`)
|
||||||
|
// Avoid overwriting existing file (e.g. retry)
|
||||||
|
const finalPath = fs.existsSync(orderVideoPath)
|
||||||
|
? path.join(this.videoDir, `${expectedOrderNumber}_${Date.now()}${ext}`)
|
||||||
|
: orderVideoPath
|
||||||
|
fs.renameSync(originalPath, finalPath)
|
||||||
|
log.debug('[视频录制] 视频已重命名', { from: originalPath, to: finalPath })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (videoError) {
|
||||||
|
log.warn('[视频录制] 重命名视频失败', {
|
||||||
|
orderNumber: expectedOrderNumber,
|
||||||
|
error: videoError instanceof Error ? videoError.message : String(videoError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user