|
|
@@ -11,6 +11,9 @@ const SIMPLE_UPLOAD_PATH = "/upload/simple";
|
|
|
/** OSS STS 临时凭证(官方相册等直传 OSS) */
|
|
|
const OSS_STS_TOKEN_PATH = "/upload/oss/sts-token";
|
|
|
|
|
|
+/** OSS 直传成功后的 AI 审核(与 sts-token 同基址 BASE_AI_URL) */
|
|
|
+const OSS_FINALIZE_PATH = "/upload/oss/finalize";
|
|
|
+
|
|
|
/** 浏览器默认可达;内网可配 VITE_OSS_UPLOAD_ENDPOINT=oss-cn-beijing-internal.aliyuncs.com */
|
|
|
const DEFAULT_OSS_UPLOAD_ENDPOINT = "oss-cn-beijing.aliyuncs.com";
|
|
|
|
|
|
@@ -563,7 +566,105 @@ async function putFileToOssWithSts(file, fetchOptions = {}) {
|
|
|
});
|
|
|
const fileUrl =
|
|
|
(typeof result.url === "string" && result.url.trim()) || buildOssObjectPublicUrl(sts.bucket, endpoint, objectKey);
|
|
|
- return { fileUrl, downloadUrl: fileUrl };
|
|
|
+ return { fileUrl, downloadUrl: fileUrl, objectKey };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @param {File} file
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+function resolveFileContentType(file) {
|
|
|
+ const t = String(file?.type ?? "").trim();
|
|
|
+ if (t) return t;
|
|
|
+ const ext = getFileExtension(file?.name, "image");
|
|
|
+ const videoMap = { mp4: "video/mp4", webm: "video/webm", mov: "video/quicktime", ogg: "video/ogg" };
|
|
|
+ if (videoMap[ext]) return videoMap[ext];
|
|
|
+ const imageMap = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
|
|
|
+ return imageMap[ext] || "image/jpeg";
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST /upload/oss/finalize(与 fetchOssStsToken 同基址、同鉴权方式)
|
|
|
+ * @param {File} file
|
|
|
+ * @param {string} objectKey
|
|
|
+ * @param {{ signal?: AbortSignal }} [fetchOptions]
|
|
|
+ * @returns {Promise<{ downloadUrl?: string }>}
|
|
|
+ */
|
|
|
+async function requestOssFinalizeAudit(file, objectKey, fetchOptions = {}) {
|
|
|
+ const base = String(BASE_AI_URL || "").replace(/\/$/, "");
|
|
|
+ if (!base) {
|
|
|
+ throw new Error("未配置上传服务地址(VITE_AI_UPLOAD_BASE 或默认 /ai-upload)");
|
|
|
+ }
|
|
|
+ const userStore = useUserStore();
|
|
|
+ const headers = {
|
|
|
+ Accept: "application/json",
|
|
|
+ "Content-Type": "application/json"
|
|
|
+ };
|
|
|
+ const token = userStore.token || "";
|
|
|
+ if (token) {
|
|
|
+ headers.Authorization = token;
|
|
|
+ }
|
|
|
+ const url = `${base}${OSS_FINALIZE_PATH}`;
|
|
|
+ let res;
|
|
|
+ try {
|
|
|
+ res = await fetch(url, {
|
|
|
+ method: "POST",
|
|
|
+ headers,
|
|
|
+ credentials: "omit",
|
|
|
+ body: JSON.stringify({
|
|
|
+ object_key: String(objectKey ?? "").trim(),
|
|
|
+ filename: String(file?.name ?? "file").trim() || "file",
|
|
|
+ content_type: resolveFileContentType(file),
|
|
|
+ size: Number(file?.size) || 0
|
|
|
+ }),
|
|
|
+ signal: fetchOptions.signal ?? undefined
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ if (isUploadUserCancelledError(err)) {
|
|
|
+ throw err instanceof Error ? err : new Error(String(err ?? "AbortError"));
|
|
|
+ }
|
|
|
+ console.error("[oss/finalize] 请求失败:", url, err);
|
|
|
+ throw new Error("内容审核请求失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ const rawText = await res.text();
|
|
|
+ let parsed = null;
|
|
|
+ const trimmed = rawText.trim();
|
|
|
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
|
+ try {
|
|
|
+ parsed = JSON.parse(rawText);
|
|
|
+ } catch (_) {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const modHint = formatSimpleUploadModerationMessage(parsed);
|
|
|
+ let msg = modHint;
|
|
|
+ if (!msg && parsed && typeof parsed === "object") {
|
|
|
+ const p = /** @type {Record<string, unknown>} */ (parsed);
|
|
|
+ const m = p.msg ?? p.message ?? p.error;
|
|
|
+ if (m != null && String(m)) msg = String(m);
|
|
|
+ }
|
|
|
+ if (!msg && trimmed) msg = trimmed.slice(0, 200);
|
|
|
+ if (!msg) msg = "内容审核请求失败";
|
|
|
+ throw new Error(msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ assertSimpleUploadBusinessOk(parsed);
|
|
|
+ } catch (bizErr) {
|
|
|
+ console.error("[oss/finalize] 业务失败", bizErr, trimmed.slice(0, 500));
|
|
|
+ throw bizErr;
|
|
|
+ }
|
|
|
+
|
|
|
+ const moderationUserTip = formatSimpleUploadModerationMessage(parsed);
|
|
|
+ if (moderationUserTip) {
|
|
|
+ throw new Error(moderationUserTip);
|
|
|
+ }
|
|
|
+
|
|
|
+ const downloadUrl = (preferDownloadUrlFromBody(parsed) || "").trim();
|
|
|
+ return downloadUrl ? { downloadUrl } : {};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -585,7 +686,13 @@ export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- const runUpload = async signal => putFileToOssWithSts(file, signal ? { signal } : {});
|
|
|
+ const runUpload = async signal => {
|
|
|
+ const fetchOpts = signal ? { signal } : {};
|
|
|
+ const uploaded = await putFileToOssWithSts(file, fetchOpts);
|
|
|
+ const audit = await requestOssFinalizeAudit(file, uploaded.objectKey, fetchOpts);
|
|
|
+ const downloadUrl = audit.downloadUrl?.trim() || uploaded.downloadUrl;
|
|
|
+ return { fileUrl: uploaded.fileUrl, downloadUrl };
|
|
|
+ };
|
|
|
|
|
|
let result;
|
|
|
if (skipSimpleUploadOverlay) {
|
|
|
@@ -606,7 +713,7 @@ export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
|
|
|
return result;
|
|
|
} catch (e) {
|
|
|
closeLoading();
|
|
|
- console.error("OSS 直传失败", e);
|
|
|
+ console.error("OSS 直传或审核失败", e);
|
|
|
if (isUploadUserCancelledError(e)) {
|
|
|
throw e;
|
|
|
}
|