Files
xiaoxia-saas/apps/web/src/api/assets.ts
T
Audit Bot 841b501c30
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
fix: 素材库新建自动获取/创建默认项目以提供 project_id
- 新增 api/projects.ts:项目 API 模块,含 getOrCreateDefaultProject
- 修改 createAssetLibrary:自动解析 project_id 再发送请求
- 后端 CreateAssetLibraryRequest 要求 project_id,前端之前未发送导致 422
- 修复 P0:新建素材库点击确定无响应

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-28 22:01:38 +08:00

237 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 素材相关 API
* Phase 1 重构:去掉 project_id,素材直接归属用户
*/
import apiClient from './client';
import { getOrCreateDefaultProject } from './projects';
/** 素材条目 */
export interface AssetItem {
id: string;
library_id: string;
name: string;
storage_key: string;
mime_type: string;
metadata: Record<string, unknown>;
file_size?: number;
status?: string;
classification_status?: string | null;
quality_score?: number | null;
created_at?: string;
}
/** 素材库 */
export interface AssetLibraryItem {
id: string;
name: string;
kind: 'video' | 'voice' | 'image';
asset_count?: number;
total_size?: number;
created_at?: string;
}
/** 入库任务 */
export interface IngestJob {
id: string;
library_id: string;
storage_key: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
error_message: string;
result_asset_id: string;
}
/** 分类任务 */
export interface ClassificationJob {
id: string;
asset_id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
classification: string;
confidence: number;
error_message: string;
}
/** 素材诊断信息 */
export interface AssetDiagnosis {
readiness_score: number;
readiness_label: string;
total_assets: number;
ready_assets: number;
video_assets: number;
image_assets: number;
voice_assets: number;
total_duration_seconds: number;
estimated_video_count: number;
used_assets: number;
unused_assets: number;
pending_review_assets: number;
smart_views: Array<{
key: string;
label: string;
count: number;
description: string;
}>;
gaps: Array<{
key: string;
severity: 'critical' | 'warning' | 'info';
message: string;
recommendation: string;
}>;
}
// ─── 素材诊断 ──────────────────────────────────────────────
/** 获取当前用户的素材诊断信息 */
export const getAssetDiagnosis = async (): Promise<AssetDiagnosis> => {
const response = await apiClient.get('/asset-diagnosis');
return response.data;
};
// ─── 素材库 ────────────────────────────────────────────────
/** 获取当前用户的所有素材库 */
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
const response = await apiClient.get('/asset-libraries');
return response.data.items || [];
};
/** 创建素材库(自动获取或创建默认项目以提供 project_id */
export const createAssetLibrary = async (data: {
name: string;
kind: 'video' | 'voice' | 'image';
}): Promise<AssetLibraryItem> => {
// 后端要求 project_id,前端自动管理默认项目
const project = await getOrCreateDefaultProject();
const response = await apiClient.post('/asset-libraries', {
project_id: project.id,
...data,
});
return response.data;
};
/** 删除素材库 */
export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
await apiClient.delete(`/asset-libraries/${libraryId}`);
};
// ─── 素材 ──────────────────────────────────────────────────
/** 获取素材库下的所有素材 */
export const getAssets = async (libraryId: string): Promise<AssetItem[]> => {
const response = await apiClient.get('/assets', {
params: { library_id: libraryId },
});
return response.data.items || [];
};
/** 更新素材审核状态 */
export const updateAssetReviewStatus = async (
assetId: string,
reviewStatus: 'pending_review' | 'approved' | 'rejected'
): Promise<AssetItem> => {
const response = await apiClient.patch(`/assets/${assetId}/review`, {
review_status: reviewStatus,
});
return response.data;
};
/** 删除素材 */
export const deleteAsset = async (assetId: string): Promise<void> => {
await apiClient.delete(`/assets/${assetId}`);
};
// ─── 上传 ──────────────────────────────────────────────────
/** 表单上传素材(小文件) */
export const uploadAsset = async (
formData: FormData
): Promise<{ storage_key: string; ingest_job_id: string; url: string }> => {
const response = await apiClient.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30 * 60 * 1000,
});
return response.data;
};
/** 预签名直传准备 */
export const prepareDirectUpload = async (data: {
library_id: string;
filename: string;
content_type: string;
file_size: number;
}): Promise<{
upload_url: string;
method: string;
storage_key: string;
expires_at: string;
fields: Record<string, string>;
max_size_bytes: number;
}> => {
const response = await apiClient.post('/upload/direct/prepare', data);
return response.data;
};
/** 直传完成确认 */
export const completeDirectUpload = async (data: {
library_id: string;
storage_key: string;
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
const response = await apiClient.post('/upload/direct/complete', data);
return response.data;
};
/** 直传上传(大文件推荐) */
export const uploadAssetDirect = async (data: {
file: File;
library_id: string;
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
const prepared = await prepareDirectUpload({
library_id: data.library_id,
filename: data.file.name,
content_type: data.file.type || 'application/octet-stream',
file_size: data.file.size,
});
const directForm = new FormData();
Object.entries(prepared.fields).forEach(([key, value]) =>
directForm.append(key, value)
);
directForm.append('file', data.file);
const uploadResponse = await fetch(prepared.upload_url, {
method: prepared.method,
body: directForm,
});
if (!uploadResponse.ok) {
throw new Error(`OSS direct upload failed: ${uploadResponse.status}`);
}
return completeDirectUpload({
library_id: data.library_id,
storage_key: prepared.storage_key,
});
};
// ─── 入库 / 分类任务 ───────────────────────────────────────
/** 查询入库任务状态 */
export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
const response = await apiClient.get(`/ingest-jobs/${jobId}`);
return response.data;
};
/** 提交素材分类任务 */
export const submitClassificationJob = async (data: {
asset_id: string;
}): Promise<ClassificationJob> => {
const response = await apiClient.post('/classification-jobs', data);
return response.data;
};
/** 查询分类任务状态 */
export const getClassificationJob = async (
jobId: string
): Promise<ClassificationJob> => {
const response = await apiClient.get(`/classification-jobs/${jobId}`);
return response.data;
};