feat: 多功能更新
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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<ILskyUploadedImage> => {
|
||||
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<ILskyUploadResponse>("/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";
|
||||
};
|
||||
Reference in New Issue
Block a user