ai-chatbot/lib/image-delivery.ts

145 lines
3.8 KiB
TypeScript

const IMAGE_PROXY_PATH = "/api/image-proxy";
type ImageProxyFormat = "webp" | "avif" | "jpeg" | "png";
export type ImageProxyOptions = {
width?: number;
quality?: number;
format?: ImageProxyFormat;
delivery?: "display" | "proxy";
};
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function normalizeWidth(width?: number): number | undefined {
if (!Number.isFinite(width) || !width) return undefined;
return clamp(Math.round(width), 64, 4096);
}
function normalizeQuality(quality?: number): number | undefined {
if (!Number.isFinite(quality) || !quality) return undefined;
return clamp(Math.round(quality), 40, 100);
}
function isAbsoluteHttpUrl(url: string): boolean {
return /^https?:\/\//i.test(url);
}
function isBypassUrl(url: string): boolean {
return (
!url ||
url.startsWith("data:") ||
url.startsWith("blob:") ||
url.startsWith(IMAGE_PROXY_PATH) ||
!isAbsoluteHttpUrl(url)
);
}
function isAliyunOssImageHost(hostname: string): boolean {
const normalized = hostname.trim().toLowerCase();
return normalized.endsWith(".aliyuncs.com") && normalized.includes(".oss-");
}
function isTrustedDirectStorageImageHost(hostname: string): boolean {
const normalized = hostname.trim().toLowerCase();
return (
normalized === "yybb.top" ||
normalized.endsWith(".qiniucs.com") ||
normalized.endsWith(".qiniudn.com") ||
normalized.endsWith(".clouddn.com") ||
normalized.endsWith(".qbox.me")
);
}
function resolveTrustedDirectStorageDisplayUrl(url: string): string | null {
try {
const target = new URL(url);
return isTrustedDirectStorageImageHost(target.hostname)
? target.toString()
: null;
} catch {
return null;
}
}
function resolveAliyunOssDisplayUrl(
url: string,
options: Pick<ImageProxyOptions, "width" | "quality" | "format">
): string | null {
let target: URL;
try {
target = new URL(url);
} catch {
return null;
}
if (!isAliyunOssImageHost(target.hostname)) {
return null;
}
if (target.searchParams.has("x-oss-process")) {
return target.toString();
}
const width = normalizeWidth(options.width);
const quality = normalizeQuality(options.quality);
const format = options.format;
if (!width && !quality && !format) {
return target.toString();
}
const processSteps = ["image"];
if (width) {
processSteps.push(`resize,w_${width}`);
}
if (quality) {
processSteps.push(`quality,q_${quality}`);
}
if (format) {
const ossFormat = format === "jpeg" ? "jpg" : format;
if (ossFormat !== "webp" && ossFormat !== "jpg" && ossFormat !== "png") {
return null;
}
processSteps.push(`format,${ossFormat}`);
}
target.searchParams.set("x-oss-process", processSteps.join("/"));
return target.toString();
}
export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {}): string {
const source = String(url || "").trim();
if (isBypassUrl(source)) {
return source;
}
const trustedDirectStorageUrl = resolveTrustedDirectStorageDisplayUrl(source);
if (trustedDirectStorageUrl) {
return trustedDirectStorageUrl;
}
if (options.delivery !== "proxy") {
const directDisplayUrl = resolveAliyunOssDisplayUrl(source, options);
if (directDisplayUrl) {
return directDisplayUrl;
}
return source;
}
const width = normalizeWidth(options.width);
const quality = normalizeQuality(options.quality);
const format = options.format;
const params = new URLSearchParams();
params.set("url", source);
if (options.delivery === "proxy") params.set("proxy", "1");
if (width) params.set("w", String(width));
if (quality) params.set("q", String(quality));
if (format) params.set("f", format);
return `${IMAGE_PROXY_PATH}?${params.toString()}`;
}