后端生成闸+多项修复;前端全站更新;QA 审计与报告
后端: - 新增 celery_health 生成前置闸——无 worker 在线时图片/视频生成入口 一律 503,防"提交到 ARK 后无人轮询、结果悬空+额度冻结"的数据丢失 - 拼接导出:帧率跟随源众数、字幕逐句重映射、原声/BGM 混音修复 - 资金核算审计脚本 + genesis 账目回填 + 滞留预留清理命令 - 接入 yunqi provider 与豆包 TTS 模型(catalog/migrations/bootstrap 命令) 前端:全站页面更新(pipeline/library/products/projects/team/account 等), 新增共享 pager 分页组件 QA:刷新 function-audit 全量输出,新增 full-qa 报告 文档:BP 产品介绍资料、design/CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ import type {
|
||||
Team,
|
||||
TeamMember,
|
||||
User,
|
||||
UserPreference
|
||||
UserPreference,
|
||||
VoiceoverInfo
|
||||
} from "./types";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || "";
|
||||
@@ -53,7 +54,17 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new ApiError(response.status, text || `${response.status} ${response.statusText}`);
|
||||
// DRF 错误体是 JSON({"detail": "..."} 或 {field: ["..."]}),提取人话给 toast,别把原始 JSON 怼到用户脸上
|
||||
let message = text || `${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const data = JSON.parse(text) as Record<string, unknown>;
|
||||
const first = data.detail ?? data.error ?? data.message ?? Object.values(data)[0];
|
||||
if (typeof first === "string") message = first;
|
||||
else if (Array.isArray(first) && typeof first[0] === "string") message = first[0];
|
||||
} catch {
|
||||
/* 非 JSON(如网关 HTML)保持原文 */
|
||||
}
|
||||
throw new ApiError(response.status, message);
|
||||
}
|
||||
if (response.status === 204) return undefined as T;
|
||||
return response.json() as Promise<T>;
|
||||
@@ -168,7 +179,7 @@ export const api = {
|
||||
deleteProject(id: string) {
|
||||
return request<void>(`/api/projects/${id}/`, { method: "DELETE" });
|
||||
},
|
||||
generateScript(projectId: string, payload: { prompt: string; selling_point_ids?: string[] }) {
|
||||
generateScript(projectId: string, payload: { prompt: string; source?: string; selling_point_ids?: string[] }) {
|
||||
return request<ScriptVersion>(`/api/projects/${projectId}/generate-script/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
@@ -180,6 +191,21 @@ export const api = {
|
||||
body: JSON.stringify({ script_version_id })
|
||||
});
|
||||
},
|
||||
updateScriptSegment(projectId: string, payload: { segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) {
|
||||
return request<ScriptVersion>(`/api/projects/${projectId}/update-script-segment/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
addScriptSegment(projectId: string, payload: { after_segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) {
|
||||
return request<ScriptVersion>(`/api/projects/${projectId}/add-script-segment/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
deleteScriptSegment(projectId: string, payload: { segment_id: string }) {
|
||||
return request<ScriptVersion>(`/api/projects/${projectId}/delete-script-segment/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
adoptVideoVersion(projectId: string, payload: { video_segment_id: string; version_id: string }) {
|
||||
return request<Project>(`/api/projects/${projectId}/adopt-video-version/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
generateVoiceover(projectId: string, payload: { items: Array<{ index: number; text: string }>; voice_type?: string; speed_ratio?: number }) {
|
||||
return request<{ voiceover: VoiceoverInfo }>(`/api/projects/${projectId}/generate-voiceover/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
generateBaseAsset(projectId: string, payload: { kind: "product" | "person" | "scene"; prompt: string }) {
|
||||
return request(`/api/projects/${projectId}/generate-base-asset/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
@@ -231,6 +257,15 @@ export const api = {
|
||||
assets() {
|
||||
return request<Paginated<Asset>>("/api/assets/");
|
||||
},
|
||||
// 经后端同源代理取资产原始文件(TOS 未配 CORS,浏览器抽帧/解码必须同源)
|
||||
async fetchAssetBlob(id: string): Promise<Blob> {
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_BASE}/api/assets/${id}/raw/`, {
|
||||
headers: token ? { Authorization: `Token ${token}` } : undefined
|
||||
});
|
||||
if (!response.ok) throw new ApiError(response.status, `fetch asset raw failed: ${response.status}`);
|
||||
return response.blob();
|
||||
},
|
||||
// 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url,
|
||||
// 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。
|
||||
async allAssets(): Promise<Asset[]> {
|
||||
|
||||
Reference in New Issue
Block a user