gongxiangp2/lib/utils.ts
chenpei 4712175cb0 feat(素材链路): 发布白底图双轨与AI生图回传能力
- 完成白底图 WebP 双轨接口、上传链路与预览缓存改造
- 新增 AI 生图平台成品回传接口、任务状态与表单规则
- 补齐 AI 生成标签、任务详情补全、相关测试与对接文档
2026-03-25 10:28:02 +08:00

224 lines
6.2 KiB
TypeScript
Raw Permalink 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 规范化工具
* 将内网地址转换为公网地址
*/
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;
}