fix(extract): 实体提取改异步(worker+轮询),根治 502 + 刷新丢 loading + 重复扣费

问题:进资产页点"提取人物/场景"——同步死等豆包思考模型(数十秒)→ 撞网关超时
502;"提取中"loading 是纯前端内存态,刷新即丢;且无幂等,刷新后重点会重复扣费。

改动(对齐全站异步三件套:出图/三视图/故事板同款):
- 后端 submit_extract_entities:Web 只建 RESERVED 任务 + 预留额度秒回(不再 502);
  已有在途提取则复用,绝不二次预扣(防刷新后重点 / 并发重复扣费)。
- 后端 run_extract_entities_task(Celery worker):流式调豆包 + 解析 + 落库 + 扣费;
  失败退费并把可读错误记进 task.error_message。
- 新增 AITask.Type.entity_extraction(迁移 0011)+ extract-status 端点(轮询进度/成败)。
- 前端 runExtract 改提交+轮询;进资产页自动认领在途提取,刷新后 loading 自己回来。

验收:完整 Django 套件 149/149 全绿(含新增 4 测:异步落库+计费一次 / content空
回退 reasoning / 在途防重复扣费 / 状态端点成败透出);tsc + vite build 全绿;
makemigrations --check 无遗漏。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-22 23:15:11 +08:00
co-authored by Claude Opus 4.8
parent 71af0ae166
commit c8cb6d3d51
8 changed files with 304 additions and 86 deletions
+10 -7
View File
@@ -327,16 +327,19 @@ export const api = {
body: JSON.stringify({ script_version_id })
});
},
// 独立实体提取:读已定稿脚本 → AI 识别角色/场景 + 每镜 refs,落库覆盖 metadata。商品不提(参考图层用真实主图)。
// 独立实体提取(**异步**):只提交任务、秒回 task_id(慢活在 worker 跑,不再 502);已有在途则复用(防重复扣费)。
// 进度/结果用 extractStatus 轮询。商品不提(参考图层用真实主图)。
extractEntities(projectId: string) {
return request<{ task_id: string; status: string }>(`/api/projects/${projectId}/extract-entities/`, { method: "POST" });
},
// 实体提取进度:running=有在途任务(刷新后据此重建 loading);否则读最近一次成败,成功回带 entities 供「提取+生成」继续出图。
extractStatus(projectId: string) {
return request<{
running: boolean;
status: string;
error: string;
entities: Array<{ id: string; type: "character" | "scene"; name: string; visual_prompt: string; ref_index: number }>;
segments: Array<{ index: number; entity_refs: string[] }>;
cast: string[];
cast_prompts: Record<string, string>;
scenes: string[];
scene_prompts: Record<string, string>;
}>(`/api/projects/${projectId}/extract-entities/`, { method: "POST" });
}>(`/api/projects/${projectId}/extract-status/`);
},
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) });