// LEGACY_READ_ONLY: 旧域名仅用于历史链接规范化。 /** * 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; }