import { createError } from "h3"; interface ILskyUploadInput { imageUrl: string; userId: number; username?: string | null; recordId: bigint; createdAt: Date; } interface ILskyUploadResponse { status?: string | boolean; message?: string; data?: { public_url?: string; filename?: string; mimetype?: string; extension?: string; [key: string]: unknown; } | null; time?: number; [key: string]: unknown; } export interface ILskyUploadedImage { publicUrl: string; filename: string; mimetype: string; response: ILskyUploadResponse; } export const uploadImageFromUrl = async ( input: ILskyUploadInput ): Promise => { const config = getLskyConfig(); const downloadedImage = await downloadImage(input.imageUrl); const extension = getImageExtension(downloadedImage.mimeType, input.imageUrl); const filename = buildArchiveFilename({ userId: input.userId, username: input.username, recordId: input.recordId, createdAt: input.createdAt, extension }); const formData = new FormData(); formData.append( "file", new Blob([downloadedImage.bytes], { type: downloadedImage.mimeType }), filename ); formData.append("storage_id", String(config.storageId)); formData.append("is_remove_exif", "true"); formData.append("intro", `AIArtStudio image generation ${input.recordId}`); for (const tag of buildArchiveTags(input.userId, input.recordId)) { formData.append("tags[]", tag); } const response = await $fetch("/upload", { baseURL: config.baseUrl, method: "POST", body: formData, headers: { Accept: "application/json", Authorization: `Bearer ${config.token}` } }); const publicUrl = response.data?.public_url; if (!isSuccessStatus(response.status) || !publicUrl) { throw createError({ statusCode: 502, statusMessage: response.message || "图床上传失败" }); } return { publicUrl, filename: response.data?.filename || filename, mimetype: response.data?.mimetype || downloadedImage.mimeType, response }; }; const getLskyConfig = () => { const baseUrl = process.env.LSKY_BASE_URL?.replace(/\/+$/, ""); const token = process.env.LSKY_TOKEN; const storageId = Number.parseInt(process.env.LSKY_STORAGE_ID ?? "", 10); if (!baseUrl) { throw new Error("LSKY_BASE_URL 未配置"); } if (!token) { throw new Error("LSKY_TOKEN 未配置"); } if (!Number.isInteger(storageId) || storageId <= 0) { throw new Error("LSKY_STORAGE_ID 未配置或不是有效数字"); } return { baseUrl, token, storageId }; }; const downloadImage = async (imageUrl: string) => { const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`图片下载失败:${response.status}`); } return { bytes: await response.arrayBuffer(), mimeType: response.headers.get("content-type") || "image/png" }; }; const buildArchiveFilename = ({ userId, username, recordId, createdAt, extension }: { userId: number; username?: string | null; recordId: bigint; createdAt: Date; extension: string; }) => { const safeUsername = sanitizeFilenamePart(username || "user"); const timestamp = formatTimestamp(createdAt); return `${userId}_${safeUsername}_${timestamp}_${recordId.toString()}.${extension}`; }; const buildArchiveTags = (userId: number, recordId: bigint) => { return [ "AIArtStudio", `user:${userId}`, `record:${recordId.toString()}`, "model:gpt-image-2" ]; }; const sanitizeFilenamePart = (value: string) => { const sanitized = value .trim() .replace(/[^\p{L}\p{N}_-]+/gu, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, ""); return sanitized || "user"; }; const formatTimestamp = (date: Date) => { return date.toISOString().replace(/\D/g, "").slice(0, 14); }; const getImageExtension = (mimeType: string, imageUrl: string) => { const fromMimeType = mimeType.split(";")[0]?.trim().toLowerCase(); if (fromMimeType === "image/jpeg") return "jpg"; if (fromMimeType === "image/png") return "png"; if (fromMimeType === "image/webp") return "webp"; if (fromMimeType === "image/gif") return "gif"; const pathname = new URL(imageUrl).pathname; const extension = pathname.split(".").pop()?.toLowerCase(); return extension && /^[a-z0-9]+$/.test(extension) ? extension : "png"; }; const isSuccessStatus = (status: unknown) => { return status === true || status === "success" || status === "ok"; };