224 lines
6.2 KiB
TypeScript
224 lines
6.2 KiB
TypeScript
/**
|
||
* URL 规范化工具
|
||
* 将内网地址转换为公网地址
|
||
*/
|
||
|
||
const LOCALHOST_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']);
|
||
|
||
// 导入代理函数(避免循环依赖,使用动态导入或直接实现)
|
||
function proxyHttpUrl(url?: string | null): string | null {
|
||
if (!url || typeof url !== 'string') return url ?? null
|
||
|
||
const trimmed = url.trim()
|
||
if (!trimmed) return null
|
||
|
||
// 非 http(s) 直接返回(可能是相对路径或 data: URL)
|
||
if (!/^https?:\/\//i.test(trimmed)) {
|
||
return trimmed
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(trimmed)
|
||
const hostname = parsed.hostname.toLowerCase()
|
||
|
||
// s3.jiansun.vip 已经有稳定入口和缓存层,优先让客户端直连。
|
||
if (hostname === 's3.jiansun.vip') {
|
||
if (parsed.protocol !== 'https:') {
|
||
parsed.protocol = 'https:'
|
||
if (parsed.port === '80') {
|
||
parsed.port = ''
|
||
}
|
||
}
|
||
return parsed.toString()
|
||
}
|
||
|
||
// 遗留公网域名列表(仍保留代理兜底)
|
||
const publicDomains = [
|
||
's3.1080p.hk',
|
||
's3.yydd.ai',
|
||
]
|
||
|
||
// 检查是否为公网域名
|
||
const isPublicDomain = publicDomains.some(domain =>
|
||
hostname === domain || hostname.endsWith(`.${domain}`)
|
||
)
|
||
|
||
// 如果是公网域名,无论 HTTP 还是 HTTPS,都通过代理提供
|
||
// 这样可以避免 ERR_CONNECTION_CLOSED 错误
|
||
if (isPublicDomain) {
|
||
// 使用相对路径的代理 API,避免硬编码域名
|
||
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`
|
||
}
|
||
|
||
// 只处理 HTTP 协议的其他资源(避免混合内容问题)
|
||
if (parsed.protocol === 'http:') {
|
||
// 其他 HTTP 资源也通过代理(如果需要)
|
||
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`
|
||
}
|
||
|
||
// 其他 HTTPS 资源直接返回
|
||
return trimmed
|
||
} catch {
|
||
// URL 解析失败时保持原样
|
||
return trimmed
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取公网访问配置
|
||
*/
|
||
function getPublicAccessConfig() {
|
||
const s3PublicDomain = (process.env.S3_PUBLIC_DOMAIN || '').trim();
|
||
const serverIp = (process.env.SERVER_IP || '').trim();
|
||
const minioApiPort = (process.env.MINIO_API_PORT || '9000').trim();
|
||
const appPublicUrl = (process.env.APP_PUBLIC_URL || process.env.NEXTAUTH_URL || '').trim();
|
||
|
||
let publicHost: string | null = null;
|
||
let publicPort = minioApiPort || '9000';
|
||
let protocol = 'http:';
|
||
|
||
// 优先使用 S3_PUBLIC_DOMAIN
|
||
if (s3PublicDomain) {
|
||
try {
|
||
const domainUrl = s3PublicDomain.match(/^https?:\/\//i)
|
||
? s3PublicDomain
|
||
: `https://${s3PublicDomain}`;
|
||
const url = new URL(domainUrl);
|
||
publicHost = url.hostname;
|
||
protocol = url.protocol;
|
||
publicPort = url.port ? url.port : (protocol === 'https:' ? '443' : '80');
|
||
|
||
// 如果是有效的公网域名,直接返回
|
||
if (publicHost && !LOCALHOST_ADDRESSES.has(publicHost)) {
|
||
// 如果是 http 协议,强制改为 https
|
||
if (protocol !== 'https:') {
|
||
protocol = 'https:';
|
||
if (!url.port) {
|
||
publicPort = '443';
|
||
}
|
||
}
|
||
return { protocol, host: publicHost, port: publicPort };
|
||
}
|
||
} catch (e) {
|
||
// 解析失败,继续尝试其他配置
|
||
}
|
||
}
|
||
|
||
// 使用 SERVER_IP
|
||
if (serverIp && !LOCALHOST_ADDRESSES.has(serverIp)) {
|
||
publicHost = serverIp;
|
||
protocol = 'http:';
|
||
publicPort = minioApiPort || '9000';
|
||
}
|
||
|
||
// 最后尝试从 APP_PUBLIC_URL 提取
|
||
if (!publicHost && appPublicUrl) {
|
||
try {
|
||
const url = new URL(appPublicUrl);
|
||
if (!LOCALHOST_ADDRESSES.has(url.hostname)) {
|
||
publicHost = url.hostname;
|
||
protocol = url.protocol;
|
||
// 如果 hostname 和 SERVER_IP 相同,使用 MinIO 端口
|
||
if (publicHost === serverIp) {
|
||
protocol = 'http:';
|
||
publicPort = minioApiPort || '9000';
|
||
} else {
|
||
publicPort = url.port || (protocol === 'https:' ? '443' : '80');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 解析失败
|
||
}
|
||
}
|
||
|
||
if (!publicHost) {
|
||
return null;
|
||
}
|
||
|
||
return { protocol, host: publicHost, port: publicPort };
|
||
}
|
||
|
||
/**
|
||
* 规范化 URL
|
||
* 将内网地址(localhost, 127.0.0.1, 192.168.x.x, 10.x.x.x)转换为公网地址
|
||
*/
|
||
export function normalizeUrl(url: string | null | undefined): string | null {
|
||
if (!url || typeof url !== 'string') {
|
||
return url ?? null;
|
||
}
|
||
|
||
const trimmedUrl = url.trim();
|
||
if (!trimmedUrl) {
|
||
return null;
|
||
}
|
||
|
||
// 如果不是 HTTP/HTTPS URL,直接返回
|
||
if (!/^https?:\/\//i.test(trimmedUrl)) {
|
||
return trimmedUrl;
|
||
}
|
||
|
||
const publicConfig = getPublicAccessConfig();
|
||
if (!publicConfig) {
|
||
return trimmedUrl;
|
||
}
|
||
|
||
try {
|
||
const urlObj = new URL(trimmedUrl);
|
||
const isLocalhost = LOCALHOST_ADDRESSES.has(urlObj.hostname);
|
||
const isPrivateIp = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(urlObj.hostname);
|
||
|
||
// 如果是内网地址,替换为公网地址
|
||
if (isLocalhost || isPrivateIp) {
|
||
urlObj.protocol = publicConfig.protocol;
|
||
urlObj.hostname = publicConfig.host;
|
||
urlObj.port = publicConfig.port;
|
||
return urlObj.toString();
|
||
}
|
||
|
||
return trimmedUrl;
|
||
} catch (e) {
|
||
return trimmedUrl;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 规范化素材对象中的所有 URL
|
||
*/
|
||
export function normalizeMaterial(material: any): any {
|
||
if (!material || typeof material !== 'object') {
|
||
return material;
|
||
}
|
||
|
||
const normalized = { ...material };
|
||
|
||
// 规范化所有 URL 字段
|
||
const urlFields = [
|
||
'previewUrl',
|
||
'videoCoverUrl',
|
||
'videoPreview540Url',
|
||
'videoPreviewClipUrl',
|
||
'videoSourceUrl',
|
||
];
|
||
|
||
urlFields.forEach(field => {
|
||
if (field in normalized) {
|
||
// 先规范化 URL(内网转公网),然后应用代理(HTTP 转 HTTPS 代理)
|
||
const normalizedUrl = normalizeUrl(normalized[field]);
|
||
normalized[field] = proxyHttpUrl(normalizedUrl);
|
||
}
|
||
});
|
||
|
||
// 规范化 displayImages 数组
|
||
if (Array.isArray(normalized.displayImages)) {
|
||
normalized.displayImages = normalized.displayImages.map((img: any) => {
|
||
if (typeof img === 'string') {
|
||
const normalizedUrl = normalizeUrl(img);
|
||
return proxyHttpUrl(normalizedUrl);
|
||
}
|
||
return img;
|
||
});
|
||
}
|
||
|
||
return normalized;
|
||
}
|