ai-chatbot/components/agents/detail-reference-upload.ts

130 lines
4.0 KiB
TypeScript

import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import type { AgentDetailReplacementReferenceItem } from "@/types/agent";
const DETAIL_REFERENCE_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
export interface UploadedDetailReferenceImage {
url: string;
name: string;
contentType: string | null;
assetId: string | null;
width: number | null;
height: number | null;
}
function isSupportedDetailReferenceImage(file: File) {
return (
file.type.startsWith("image/") ||
/\.(jpe?g|png|webp|heic|heif)$/i.test(file.name)
);
}
function buildDetailReferenceUploadFilename(file: File) {
const extension = file.name.toLowerCase().split(".").pop() || "jpg";
const baseName =
file.name
.replace(/\.[^.]+$/, "")
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48) || "target-detail";
return `${baseName}-${Date.now()}.${extension}`;
}
function normalizeDimension(value: unknown) {
const numberValue = Math.round(Number(value));
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : null;
}
async function readLocalImageDimensions(file: File) {
if (typeof window === "undefined") {
return { width: null, height: null };
}
return new Promise<{ width: number | null; height: number | null }>((resolve) => {
const objectUrl = window.URL.createObjectURL(file);
const image = new Image();
image.onload = () => {
const width = normalizeDimension(image.naturalWidth);
const height = normalizeDimension(image.naturalHeight);
window.URL.revokeObjectURL(objectUrl);
resolve({ width, height });
};
image.onerror = () => {
window.URL.revokeObjectURL(objectUrl);
resolve({ width: null, height: null });
};
image.src = objectUrl;
});
}
export async function uploadDetailReferenceImageFile(
file: File
): Promise<UploadedDetailReferenceImage> {
if (!isSupportedDetailReferenceImage(file)) {
throw new Error("请上传图片文件");
}
if (file.size > DETAIL_REFERENCE_UPLOAD_MAX_BYTES) {
throw new Error("目标详情图过大,请压缩到 50MB 以内");
}
const dimensionsPromise = readLocalImageDimensions(file);
const formData = new FormData();
formData.append("file", file, buildDetailReferenceUploadFilename(file));
formData.append("purpose", "canvas-upload");
const response = await fetchUploadWithRetry(formData);
const payload = await response.json().catch(() => null);
if (!response.ok || !payload?.url) {
throw new Error(payload?.error || "目标详情图上传失败");
}
const url = String(payload.url || "").trim();
if (!/^https?:\/\//i.test(url)) {
throw new Error("目标详情图上传后未返回可用地址");
}
const localDimensions = await dimensionsPromise;
return {
url,
name: file.name || "目标详情图",
contentType: file.type || null,
assetId: typeof payload.assetId === "string" ? payload.assetId : null,
width: normalizeDimension(payload.width) || localDimensions.width,
height: normalizeDimension(payload.height) || localDimensions.height,
};
}
export function createUploadedDetailReferenceCard(params: {
upload: UploadedDetailReferenceImage;
index: number;
labelPrefix?: string;
}): AgentDetailReplacementReferenceItem {
const labelPrefix = params.labelPrefix || "上传目标详情";
return {
id: `manual-detail-reference-${Date.now()}-${params.index + 1}-${Math.random()
.toString(36)
.slice(2, 8)}`,
label: `${labelPrefix} ${String(params.index + 1).padStart(2, "0")}`,
imageUrl: params.upload.url,
previewUrl: params.upload.url,
width: params.upload.width,
height: params.upload.height,
candidateId: null,
sortOrder: params.index,
passthrough: false,
};
}
export function applyUploadedDetailReferenceToCard(
card: AgentDetailReplacementReferenceItem,
upload: UploadedDetailReferenceImage
): AgentDetailReplacementReferenceItem {
return {
...card,
imageUrl: upload.url,
previewUrl: upload.url,
width: upload.width,
height: upload.height,
};
}