- 反编译 app/api/materials/route.ts (素材列表和删除API) - 反编译 app/api/materials/stats/route.ts (统计API) - 新增 lib/material-log.ts (素材操作日志工具) - 新增 lib/utils.ts (URL规范化工具) 这是从生产系统2.2.0版本编译代码反编译恢复的第一批文件
159 lines
4.2 KiB
TypeScript
159 lines
4.2 KiB
TypeScript
/**
|
||
* URL 规范化工具
|
||
* 将内网地址转换为公网地址
|
||
*/
|
||
|
||
const LOCALHOST_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']);
|
||
|
||
/**
|
||
* 获取公网访问配置
|
||
*/
|
||
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) {
|
||
normalized[field] = normalizeUrl(normalized[field]);
|
||
}
|
||
});
|
||
|
||
// 规范化 displayImages 数组
|
||
if (Array.isArray(normalized.displayImages)) {
|
||
normalized.displayImages = normalized.displayImages.map(img =>
|
||
typeof img === 'string' ? normalizeUrl(img) : img
|
||
);
|
||
}
|
||
|
||
return normalized;
|
||
}
|