gongxiangp2/scripts/diagnose-video-loading.ts

236 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 诊断视频加载失败问题
* 检查数据库中视频URL对应的文件是否真的存在于MinIO
*/
import { PrismaClient } from '@prisma/client'
import { S3Client, HeadObjectCommand } from '@aws-sdk/client-s3'
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://gongxiangpan:gongxiangpan123@192.168.58.20:5432/lobechat'
const prisma = new PrismaClient({
datasources: {
db: {
url: DATABASE_URL,
},
},
})
// MinIO客户端(内网)
const minioClient = new S3Client({
endpoint: process.env.S3_ENDPOINT || 'http://192.168.58.20:9000',
region: process.env.S3_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
})
const S3_BUCKET = process.env.S3_BUCKET || 'gongxiangpan'
const S3_FILE_PATH = process.env.S3_FILE_PATH || '由壹点AI/'
/**
* 从URL提取MinIO Key(路径)
*/
function extractKeyFromUrl(url: string): string | null {
try {
const urlObj = new URL(url)
// 移除开头的斜杠
let path = urlObj.pathname.replace(/^\//, '')
// 如果路径包含bucket名称,移除它
if (path.startsWith(`${S3_BUCKET}/`)) {
path = path.replace(`${S3_BUCKET}/`, '')
}
// 解码URL编码的路径
try {
path = decodeURIComponent(path)
} catch {
// 如果解码失败,使用原始路径
}
return path
} catch (error) {
console.error(`[Diagnose] Failed to parse URL: ${url}`, error)
return null
}
}
/**
* 检查MinIO文件是否存在
*/
async function checkFileExists(key: string): Promise<{ exists: boolean; error?: string }> {
try {
const command = new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: key,
})
await minioClient.send(command)
return { exists: true }
} catch (error: any) {
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
return { exists: false, error: '文件不存在' }
}
if (error.$metadata?.httpStatusCode === 403) {
return { exists: false, error: '权限不足' }
}
return { exists: false, error: error.message || String(error) }
}
}
/**
* 尝试多个可能的Key路径
*/
async function findFileInMinIO(originalUrl: string): Promise<{ key: string; exists: boolean; error?: string } | null> {
const baseKey = extractKeyFromUrl(originalUrl)
if (!baseKey) {
return { key: '', exists: false, error: '无法解析URL' }
}
// 尝试多个可能的路径
const possibleKeys = [
baseKey, // 原始路径
baseKey.replace(/^由壹点AI\//, ''), // 移除前缀
`${S3_FILE_PATH}${baseKey}`, // 添加前缀
`${S3_FILE_PATH}${baseKey.replace(/^由壹点AI\//, '')}`, // 移除并重新添加前缀
]
// 去重
const uniqueKeys = Array.from(new Set(possibleKeys))
for (const key of uniqueKeys) {
const result = await checkFileExists(key)
if (result.exists) {
return { key, exists: true }
}
}
return { key: baseKey, exists: false, error: '在所有可能路径中都未找到文件' }
}
async function main() {
console.log('开始诊断视频加载失败问题...')
console.log('='.repeat(60))
console.log()
try {
// 查询有视频URL但可能加载失败的素材
const materials = await prisma.material.findMany({
where: {
OR: [
{ videoPreviewClipUrl: { not: null } },
{ videoPreview540Url: { not: null } },
{ videoCoverUrl: { not: null } },
],
deletedAt: null,
},
select: {
id: true,
productModel: true,
materialType: true,
videoPreviewClipUrl: true,
videoPreview540Url: true,
videoCoverUrl: true,
previewUrl: true,
},
take: 20, // 先检查前20个
})
console.log(`找到 ${materials.length} 个视频素材`)
console.log()
const issues: Array<{
materialId: string
productModel: string
field: string
url: string
key: string
error: string
}> = []
for (const material of materials) {
console.log(`检查素材: ${material.productModel} (${material.id})`)
// 检查 videoPreviewClipUrl
if (material.videoPreviewClipUrl) {
const result = await findFileInMinIO(material.videoPreviewClipUrl)
if (!result || !result.exists) {
issues.push({
materialId: material.id,
productModel: material.productModel,
field: 'videoPreviewClipUrl',
url: material.videoPreviewClipUrl,
key: result?.key || 'unknown',
error: result?.error || '未知错误',
})
console.log(` ❌ videoPreviewClipUrl 不存在: ${material.videoPreviewClipUrl}`)
} else {
console.log(` ✅ videoPreviewClipUrl 存在: ${result.key}`)
}
}
// 检查 videoPreview540Url
if (material.videoPreview540Url) {
const result = await findFileInMinIO(material.videoPreview540Url)
if (!result || !result.exists) {
issues.push({
materialId: material.id,
productModel: material.productModel,
field: 'videoPreview540Url',
url: material.videoPreview540Url,
key: result?.key || 'unknown',
error: result?.error || '未知错误',
})
console.log(` ❌ videoPreview540Url 不存在: ${material.videoPreview540Url}`)
} else {
console.log(` ✅ videoPreview540Url 存在: ${result.key}`)
}
}
// 检查 videoCoverUrl
if (material.videoCoverUrl) {
const result = await findFileInMinIO(material.videoCoverUrl)
if (!result || !result.exists) {
issues.push({
materialId: material.id,
productModel: material.productModel,
field: 'videoCoverUrl',
url: material.videoCoverUrl,
key: result?.key || 'unknown',
error: result?.error || '未知错误',
})
console.log(` ❌ videoCoverUrl 不存在: ${material.videoCoverUrl}`)
} else {
console.log(` ✅ videoCoverUrl 存在: ${result.key}`)
}
}
console.log()
}
console.log('='.repeat(60))
console.log(`诊断完成,发现 ${issues.length} 个问题`)
console.log()
if (issues.length > 0) {
console.log('问题详情:')
issues.forEach((issue, index) => {
console.log(`${index + 1}. ${issue.productModel} (${issue.field})`)
console.log(` URL: ${issue.url}`)
console.log(` Key: ${issue.key}`)
console.log(` 错误: ${issue.error}`)
console.log()
})
}
} catch (error) {
console.error('诊断失败:', error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
main()