diff --git a/apps/api/app/api/routes/upload.py b/apps/api/app/api/routes/upload.py index c67add1eb..687c0db8f 100644 --- a/apps/api/app/api/routes/upload.py +++ b/apps/api/app/api/routes/upload.py @@ -206,6 +206,7 @@ async def complete_direct_upload( ingest_job_id="", duplicated=True, asset_id=existing.id, + url=storage_service.get_url(normalized_key), ) job = _submit_ingest_job( @@ -215,7 +216,7 @@ async def complete_direct_upload( ingest_job_repository=ingest_job_repository, file_hash=request.file_hash, ) - return DirectUploadCompleteResponse(storage_key=normalized_key, ingest_job_id=job.id) + return DirectUploadCompleteResponse(storage_key=normalized_key, ingest_job_id=job.id, url=storage_service.get_url(normalized_key)) @router.post( diff --git a/apps/api/app/schemas/upload.py b/apps/api/app/schemas/upload.py index b9763606c..c6d798288 100644 --- a/apps/api/app/schemas/upload.py +++ b/apps/api/app/schemas/upload.py @@ -39,6 +39,7 @@ class DirectUploadCompleteResponse(BaseModel): ingest_job_id: str duplicated: bool = Field(default=False, description="是否为重复素材(命中去重)") asset_id: str = Field(default="", description="重复素材的 asset_id(duplicated=true 时返回)") + url: str = Field(default="", description="Public URL of uploaded file") class UploadAssetResponse(BaseModel): diff --git a/apps/web/src/api/assets/index.ts b/apps/web/src/api/assets/index.ts index ba460863d..5c536f2c2 100644 --- a/apps/web/src/api/assets/index.ts +++ b/apps/web/src/api/assets/index.ts @@ -13,7 +13,6 @@ export type { ClassificationJob, AssetDiagnosis, BatchOperationResult, - UploadResult, DirectUploadPrepareResult, DirectUploadCompleteResult, } from "./types" @@ -40,7 +39,7 @@ export { } from "./assets" // 上传 -export { uploadAsset, prepareDirectUpload, completeDirectUpload, uploadAssetDirect } from "./upload" +export { prepareDirectUpload, completeDirectUpload, uploadAssetDirect } from "./upload" // 任务 export { getIngestJob, submitClassificationJob, getClassificationJob } from "./jobs" diff --git a/apps/web/src/api/assets/types.ts b/apps/web/src/api/assets/types.ts index 9e64a942e..e98274a18 100644 --- a/apps/web/src/api/assets/types.ts +++ b/apps/web/src/api/assets/types.ts @@ -135,4 +135,5 @@ export interface DirectUploadPrepareResult { export interface DirectUploadCompleteResult { storage_key: string ingest_job_id: string + url: string } diff --git a/apps/web/src/api/assets/upload.ts b/apps/web/src/api/assets/upload.ts index 874861f1f..12304db42 100644 --- a/apps/web/src/api/assets/upload.ts +++ b/apps/web/src/api/assets/upload.ts @@ -3,16 +3,7 @@ */ import apiClient from "../client" import { getOrCreateDefaultProject } from "../projects" -import type { UploadResult, DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types" - -/** 表单上传素材(小文件) */ -export const uploadAsset = async (formData: FormData): Promise => { - const response = await apiClient.post("/upload", formData, { - headers: { "Content-Type": "multipart/form-data" }, - timeout: 30 * 60 * 1000, - }) - return response.data -} +import type { DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types" /** 预签名直传准备 */ export const prepareDirectUpload = async (data: { diff --git a/apps/web/src/components/voice/CloneModal/index.tsx b/apps/web/src/components/voice/CloneModal/index.tsx index 5aab11555..28f2beee9 100644 --- a/apps/web/src/components/voice/CloneModal/index.tsx +++ b/apps/web/src/components/voice/CloneModal/index.tsx @@ -1,7 +1,8 @@ import React, { useState, useCallback, useRef, useEffect } from "react" import { Modal, Button } from "@/components/ui" import { createVoiceClone, toVoiceClone } from "@/api/voice-clone" -import { uploadAsset } from "@/api/assets" +import { uploadAssetDirect, ensureDefaultLibrary } from "@/api/assets" +import { getOrCreateDefaultProject } from "@/api/projects" import { PROGRESS_STEPS, ACCEPTED_MIME } from "./constants" import { validateFile } from "./utils" import { useAudioRecorder } from "./hooks/useAudioRecorder" @@ -181,9 +182,15 @@ const CloneModal: React.FC = ({ open, onClose, onSuccess }) => }) } - const formData = new FormData() - formData.append("file", fileToUpload) - const uploadResult = await uploadAsset(formData) + // 获取默认项目和素材库 + const project = await getOrCreateDefaultProject() + const library = await ensureDefaultLibrary({ project_id: project.id, kind: "voice" }) + + // 直传到 OSS + const uploadResult = await uploadAssetDirect({ + file: fileToUpload, + library_id: library.id, + }) // 组件已卸载则中止后续操作 if (!isMountedRef.current) return diff --git a/apps/web/src/components/voice/hooks/useCloneSubmit.ts b/apps/web/src/components/voice/hooks/useCloneSubmit.ts index f578f5fe2..dd38e52cc 100755 --- a/apps/web/src/components/voice/hooks/useCloneSubmit.ts +++ b/apps/web/src/components/voice/hooks/useCloneSubmit.ts @@ -1,6 +1,6 @@ import { useRef, useCallback, useEffect } from "react" import { createVoiceClone, toVoiceClone } from "@/api/voice-clone" -import { uploadAsset, ensureDefaultLibrary } from "@/api/assets" +import { uploadAssetDirect, ensureDefaultLibrary } from "@/api/assets" import { getOrCreateDefaultProject } from "@/api/projects" import type { VoiceClone } from "@/api/voice-clone" @@ -62,15 +62,15 @@ export function useCloneSubmit({ }) } - // 获取默认项目和素材库(后端 /upload 接口必填) + // 获取默认项目和素材库 const project = await getOrCreateDefaultProject() const library = await ensureDefaultLibrary({ project_id: project.id, kind: "voice" }) - const formData = new FormData() - formData.append("file", fileToUpload) - formData.append("project_id", project.id) - formData.append("library_id", library.id) - const uploadResult = await uploadAsset(formData) + // 直传到 OSS + const uploadResult = await uploadAssetDirect({ + file: fileToUpload, + library_id: library.id, + }) // 阶段 2:克隆 setPhase("cloning") diff --git a/apps/web/src/test/api/assets.test.ts b/apps/web/src/test/api/assets.test.ts index 4745fcdf5..71f5788d7 100644 --- a/apps/web/src/test/api/assets.test.ts +++ b/apps/web/src/test/api/assets.test.ts @@ -10,7 +10,6 @@ import { updateAsset, updateAssetReviewStatus, deleteAsset, - uploadAsset, prepareDirectUpload, completeDirectUpload, uploadAssetDirect, @@ -222,22 +221,6 @@ describe("assets API", () => { }) }) - describe("uploadAsset", () => { - it("should resolve successfully", async () => { - await expect(uploadAsset(new FormData())).resolves.not.toThrow() - }) - - it("should reject on API error", async () => { - mockGet.mockRejectedValue(new Error("Network error")) - mockPost.mockRejectedValue(new Error("Network error")) - mockPut.mockRejectedValue(new Error("Network error")) - mockDelete.mockRejectedValue(new Error("Network error")) - mockPatch.mockRejectedValue(new Error("Network error")) - - await expect(uploadAsset(new FormData())).rejects.toThrow() - }) - }) - describe("prepareDirectUpload", () => { it("should resolve successfully", async () => { await expect(prepareDirectUpload({ name: "test-item" })).resolves.not.toThrow() diff --git a/apps/web/src/test/components/voice/CloneModal.test.tsx b/apps/web/src/test/components/voice/CloneModal.test.tsx index 91847c07c..47fcf8345 100755 --- a/apps/web/src/test/components/voice/CloneModal.test.tsx +++ b/apps/web/src/test/components/voice/CloneModal.test.tsx @@ -10,7 +10,9 @@ vi.mock("@/api/voice-clone", () => ({ })) vi.mock("@/api/assets", () => ({ - uploadAsset: vi.fn(), + uploadAssetDirect: vi + .fn() + .mockResolvedValue({ storage_key: "test", ingest_job_id: "test", url: "http://test" }), })) vi.mock("@/components/ui", () => ({