fix: 电商测试清单一轮 bug 修复(视频项目角色/场景 + 图片工作台 + 视频收口)
视频项目(ZWQ): - 演员工作台保存后「我的演员」仍 0 个:保存时加入资产库(in_library=True) - 资产详情「应用当前角色」自动送审(不再要手动点灰盾) - 「我的演员」卡 + 待生成 seed 卡补删除入口(垃圾桶,两步确认) - 新增场景未生成就刷新会丢:sceneDrafts 按项目持久化 localStorage - 场景 AI 生成强制叠加「无真人」约束,避免出真人致视频被火山拒 - 进入项目默认落到当前阶段(不再恒显第 1 阶段) - 视频已出但进度卡 3/4:submit/poll/adopt 幂等补推 current_stage→VIDEO, 否则 settle_video_completion 因阶段守卫直接返回、永不收口 completed 图片工作台(PMC): - 生图走 action 全局单飞锁致并发重跑/多平台套图被拒成「失败/少出图」: action 加 concurrent 选项,生图不占单飞锁 - 切模块再切回状态丢/数量少:每批各自落盘 pendingIds(onSubmitted 回传), 切回时全部批次各自续轮询 附:含一处早前未提交的图片创作改动(选火山+传参考图强制切 gpt-image-2 + gen 气泡溢出 CSS)。 新增/通过测试:AttachOfficialModel、商品图删除、视频卡阶段收口、参考图强切模型。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -436,12 +436,16 @@ export function App() {
|
||||
// 用 ref 而非 loading state,避免闭包拿到旧值;同步置位,任何同一 tick 的二次点击都拦得住。
|
||||
const actionInFlightRef = useRef(false);
|
||||
|
||||
async function action<T>(work: () => Promise<T>, successText: string, opts?: { liteRefresh?: boolean }): Promise<T | null> {
|
||||
if (actionInFlightRef.current) {
|
||||
setNotice({ type: "error", text: "操作进行中,请稍候…" });
|
||||
return null;
|
||||
async function action<T>(work: () => Promise<T>, successText: string, opts?: { liteRefresh?: boolean; concurrent?: boolean }): Promise<T | null> {
|
||||
// concurrent=true:可并行的长任务(图片生成/重跑——工作台明确支持多批并行),不参与全局单飞锁,
|
||||
// 否则「正在生成时点另一张重跑」会被单飞锁拒成 null → 该批被标「失败」(PMC#8)。
|
||||
if (!opts?.concurrent) {
|
||||
if (actionInFlightRef.current) {
|
||||
setNotice({ type: "error", text: "操作进行中,请稍候…" });
|
||||
return null;
|
||||
}
|
||||
actionInFlightRef.current = true;
|
||||
}
|
||||
actionInFlightRef.current = true;
|
||||
setLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
@@ -459,7 +463,7 @@ export function App() {
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
actionInFlightRef.current = false;
|
||||
if (!opts?.concurrent) actionInFlightRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,14 +505,17 @@ export function App() {
|
||||
if (res) setUser(res);
|
||||
}
|
||||
|
||||
function generateImages(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; ratio?: string; image_model?: string; conversation_id?: string; reference_image_ids?: string[] }) {
|
||||
function generateImages(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; ratio?: string; image_model?: string; conversation_id?: string; reference_image_ids?: string[]; onSubmitted?: (taskIds: string[]) => void }) {
|
||||
// 异步生图:提交后立刻拿到任务列表,前端轮询直到出图。慢的 ARK 出图在 Celery worker 里跑——
|
||||
// Web 层不被 ~30s 请求占住 → 健康探针不饿死 → 根治"几张图整站 502";且提交成功后浏览器关掉/断网,
|
||||
// worker 仍会把图生成并落库(扣费/退费在 worker 内闭环),重开素材库即可见。
|
||||
const { onSubmitted, ...apiPayload } = payload; // onSubmitted 是本地回调,不发后端
|
||||
return action(async () => {
|
||||
const { tasks, conversation_id } = await api.submitGenerateImage(payload);
|
||||
const { tasks, conversation_id } = await api.submitGenerateImage(apiPayload);
|
||||
const ids = tasks.map((t) => t.id);
|
||||
if (ids.length === 0) throw new Error("未能提交生成任务");
|
||||
// 把刚提交的任务 id 回传给工作台,让它按「批」各自落盘 pending → 切走再回来每一批都能恢复并续轮询(PMC#5/#10)
|
||||
onSubmitted?.(ids);
|
||||
// 提交成功即落盘:刷新页面也能恢复"生成中"并继续轮询(任务在 worker 里跑,关掉浏览器也不丢)
|
||||
// 记下本批所属商品(id+名),恢复在途批次时用它当导航头,而不是用「当前选中商品」(切走再回会显示错名)
|
||||
const batchProduct = products.find((p) => p.id === payload.product_id);
|
||||
@@ -516,7 +523,8 @@ export function App() {
|
||||
const res = await pollImageTasks(payload.mode, ids);
|
||||
// 回传后端归属/新建的对话 id,供工作室把它登记进左栏会话列表并设为 active
|
||||
return { ...res, conversation_id };
|
||||
}, "图片已生成");
|
||||
// concurrent:生图支持多批并行,不占全局单飞锁(否则并发重跑被拒成 null→标失败,PMC#8)
|
||||
}, "图片已生成", { concurrent: true });
|
||||
}
|
||||
|
||||
// 轮询一批已提交的生图任务直到全部出图/超时;每出一张就回写本地,刷新后可恢复。
|
||||
|
||||
Reference in New Issue
Block a user