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:
Misaka_Company
2026-04-17 14:34:12 +08:00
parent 70957cf4ae
commit 6d65f49f2c
2 changed files with 35 additions and 5 deletions

View File

@@ -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 })
} }

View File

@@ -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)
})
}
}
} }
} }