feat(core): 火山人像素材库审核(绿/红盾)接入 + 端到端验证

- assets_client.py:照搬 AirDrama,volcengine SDK + AK/SK 签名(凭证走 settings.ASSETS_API)。
- review.py:真人资产送审/轮询编排。一团队一组(AssetReviewGroup,OneToOne),全 best-effort 不破坏主流程。
- 模型:AssetReviewGroup + Asset.review_status/review_remote_id/review_error(迁移 assets/0003)。
- 集成:真人基础资产生成后 transaction.on_commit 静默送审(services.generate_base_asset kind==person)。
- 端点 poll-reviews + 序列化暴露 review_status;前端基础资产趴每8s轮询、人物卡渲染 审核✓(绿)/审核✗·重生(红)/审核中,审核终态后停轮询。
- e2e 已验:送审→建组→processing→active(真拿到火山绿盾)。

对抗式交叉验证(2审查员,0 critical)修复:
- get_or_create_team_group 并发竞态:DB 唯一约束去重 + select_for_update 串行化远程建组,杜绝重复建组/丢送审。
- create_asset 返回空 Id 不再标 processing(否则卡死黄);processing 超 15 分钟兜底判 failed,未知 Status 记日志,防永久 processing + 无限轮询。

凭证暂借 AirDrama,.env 改 ASSETS_API_* 两行即可换;ASSETS_API_ENABLED=false 可一键关停。回归 18/18 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-17 10:51:23 +08:00
co-authored by Claude Opus 4.8
parent 521f5f4535
commit b30eaed838
14 changed files with 375 additions and 14 deletions
+32
View File
@@ -544,6 +544,30 @@ export function PipelinePage(props: {
const initHash = typeof location !== "undefined" ? location.hash.match(/#stage-(\d)/) : null;
const [viewStage, setViewStage] = useState(initHash ? Number(initHash[1]) : 1);
const [navigated, setNavigated] = useState(Boolean(initHash));
// 火山人像审核状态(真人资产绿/红盾):本地覆盖 map(轮询刷新),回退 asset.review_status
const [reviews, setReviews] = useState<Record<string, string>>({});
const assetReview = (id?: string | null) => (id ? (reviews[id] || byId.get(id)?.review_status || "") : "");
// 在基础资产趴(stage 2)定时轮询审核状态,刷新徽章(processing → active绿 / failed红)
useEffect(() => {
if (viewStage !== 2) return;
let alive = true;
let sawProcessing = false;
let timer = 0;
const tick = async () => {
const r = await api.pollReviews(project.id).catch(() => null);
if (!alive) return;
const map = (r?.reviews || {}) as Record<string, string>;
if (Object.keys(map).length) {
sawProcessing = true;
setReviews((m) => ({ ...m, ...map }));
} else if (sawProcessing) {
window.clearInterval(timer); // 审核都终态了,停轮询省空跑(后端无 processing 资产时返回 {})
}
};
void tick();
timer = window.setInterval(tick, 8000);
return () => { alive = false; window.clearInterval(timer); };
}, [viewStage, project.id]);
// 外部 hash 变化(浏览器前进/后退、地址栏改 #stage-N)也要切阶段——镜像有 hashchange 监听,这里补齐
useEffect(() => {
function onHashChange() {
@@ -2184,6 +2208,14 @@ export function PipelinePage(props: {
{group.adopted_asset
? <span className="pill ok"><span className="dot"></span></span>
: <span className="pill neutral"><span className="dot"></span></span>}
{/* 火山真人审核绿/红盾(仅人物已采用资产) */}
{kind === "person" && group.adopted_asset ? (() => {
const rs = assetReview(group.adopted_asset);
if (rs === "active") return <span className="pill ok" title="火山真人审核已通过" style={{ marginLeft: 4 }}><span className="dot"></span></span>;
if (rs === "failed") return <span className="pill err" title={`审核未过:${byId.get(group.adopted_asset)?.review_error || "建议改提示词重新生成"}`} style={{ marginLeft: 4 }}><span className="dot"></span>·</span>;
if (rs === "processing") return <span className="pill neutral" style={{ marginLeft: 4 }}><span className="dot"></span></span>;
return null;
})() : null}
</div>
{/* 行38 · 可编辑提示词:改完点重跑/替换据此生成 */}
<textarea