236 lines
6.8 KiB
TypeScript
236 lines
6.8 KiB
TypeScript
/**
|
||
* 诊断视频加载失败问题
|
||
* 检查数据库中视频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()
|