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:
zyc
2026-06-27 15:11:29 +08:00
co-authored by Claude Opus 4.8
parent ce7eaefe1d
commit 8f32ad69d3
9 changed files with 253 additions and 97 deletions
+34 -3
View File
@@ -134,21 +134,39 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
if (r) setTriReady(true);
} finally { setBusy(false); }
}
// 保存人物:写名字(若有改名能力)→ 刷新 → 关工作台 → 切「我的演员」tab
// 保存人物:写名字(若有改名能力)→ 加入资产库 → 刷新 → 关工作台 → 切「我的演员」tab
async function saveActor() {
if (!picked) return;
setBusy(true);
try {
const name = actorName.trim();
if (name && name !== picked.name && onRename) await onRename(picked.id, name);
// 关键:AI 生成/上传得到的立绘默认 in_library=False(工作台暂存),不入「我的演员」列表(只列已入库 person)。
// 「保存人物」必须把它加入资产库,否则保存后「我的演员」始终 0 个演员(ZWQ#6)。
await api.setAssetLibrary(picked.id, true).catch(() => undefined);
onRefresh();
void reload(); // 新保存的人物即时进「我的演员」列表(自取,不依赖父级全局 assets)
await reload(); // 新保存的人物即时进「我的演员」列表(自取,不依赖父级全局 assets)
setStudio(false);
setCandidates([]); setPicked(null); setActorName(""); setTriReady(false);
setTab("mine");
} finally { setBusy(false); }
}
// 删除「我的演员」(软删 person 资产)· 两步确认,3s 自动撤销(ZWQ#8)
const [delArmId, setDelArmId] = useState<string | null>(null);
const [delBusyId, setDelBusyId] = useState<string | null>(null);
async function deleteActor(id: string) {
setDelBusyId(id);
try {
await api.deleteAsset(id);
onRefresh();
await reload();
} catch { /* 删除失败保留卡片,可重试 */ } finally {
setDelBusyId(null);
setDelArmId(null);
}
}
if (!open) return null;
return createPortal(
<div className="actorlib-bg" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
@@ -262,8 +280,21 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
<div className="actorlib-grid">
{pageList.map((a) => {
const url = previewOf(a);
// 「我的演员」(非模特库预设)可删:右上角垃圾桶,两步确认(ZWQ#8)
const canDelete = tab === "mine" && !isPreset(a);
return (
<div className="actor-card" key={a.id}>
<div className="actor-card" key={a.id} style={{ position: "relative" }}>
{canDelete && (delArmId === a.id ? (
<span className="asset-card-del-confirm" data-stop style={{ position: "absolute", top: 6, right: 6, zIndex: 3, display: "flex", gap: 4 }}>
<button type="button" className="del-yes" disabled={delBusyId === a.id} onClick={(e) => { e.stopPropagation(); void deleteActor(a.id); }}>{delBusyId === a.id ? "删除中…" : "确认删除"}</button>
<button type="button" className="del-no" onClick={(e) => { e.stopPropagation(); setDelArmId(null); }}></button>
</span>
) : (
<button type="button" className="asset-card-del" title="删除该演员" style={{ position: "absolute", top: 6, right: 6, zIndex: 3 }}
onClick={(e) => { e.stopPropagation(); setDelArmId(a.id); window.setTimeout(() => setDelArmId((v) => (v === a.id ? null : v)), 3000); }}>
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2" /><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6" /></svg>
</button>
))}
<div className={`placeholder actor-thumb${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}
role="button" tabIndex={0} title={mode === "replace" ? "选用此演员" : "查看大图"}
onClick={() => { if (mode === "replace") { void onPick?.(a.id, a.name); } else if (url) { setPreview({ src: url, name: a.name }); } }}