生图模型可选(火山/gpt-image)+ 模特上身图提示词强化 + 多会话等改动

本轮(生图模型选择 + 火山接入):
- 工作室新增「生图模型」选择器(模特上身图/平台套图头部 chip + 图片创作底部 Pill),
  默认火山 Seedream,可切 gpt-image-2;选择写入 localStorage,下次进页面读回
- 后端 resolve_image_model 解析所选模型;enqueue_standalone_images 接 image_model
- worker 按模型能力分流:有 image_edit(gpt-image)走多图编辑;无(火山)走
  image_generation(image=参考图);新增 _ratio_to_volcano_size 让火山按比例出图
  → 内衣等敏感品类用火山可绕开 gpt-image 的 sexual 内容审核

模特上身图提示词:
- 穿戴/非穿戴分流、按 index 变化动作场景镜头、负面词尾接、多图参考序号自适应
- _product_reference_urls:商品参考真实上传图优先、排除 AI 生成图、可多张

其他(并入此前各会话未提交改动):
- 图片创作多会话(ImageConversation + migration 0019)、任务中心按类型过滤
- accounts/projects/assets/team/auth 等零散调整、相关测试
- 测试脚本(tryon_*.py)、测试清单(core/bug/*.xlsx)
This commit is contained in:
zyc
2026-06-27 14:00:28 +08:00
parent 0a519ca892
commit ce7eaefe1d
21 changed files with 1279 additions and 158 deletions
+33 -10
View File
@@ -103,6 +103,9 @@ export function App() {
const [user, setUser] = useState<User | null>(null);
const [team, setTeam] = useState<Team | null>(null);
// 当前用户在该团队的角色(owner/admin/member)。主账号(owner=超管)才看得到「团队」「消费」页(PMC#3)。
const [role, setRole] = useState<string>("");
const isOwner = role === "owner";
const [products, setProducts] = useState<Product[]>([]);
const [productTotal, setProductTotal] = useState(0); // 后端真实总数(分页 count),侧栏/仪表盘徽标用
const [projects, setProjects] = useState<Project[]>([]);
@@ -232,6 +235,7 @@ export function App() {
if (cancelled || !identity) return;
setUser(identity.user);
setTeam(identity.team);
setRole(identity.role || "");
} catch (bootError) {
// 仅「身份校验失败」才登出;me() 成功后的数据加载失败不在此处
console.error("[boot] identity failed:", bootError);
@@ -280,6 +284,16 @@ export function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [booting, user, team, route.admin]);
// 子账号(非主账号)纠回:团队 / 消费 页仅主账号(owner=超管)可见,子账号直接拉回工作台(PMC#3)。
// role 为空时(身份尚未带回角色)不拦,避免误纠超管。
useEffect(() => {
if (booting || !user || !role) return;
if (!isOwner && (page === "team" || page === "account")) {
navigate("dashboard", { replace: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [booting, user, role, isOwner, page]);
// Load preferences + sessions when entering settings.
useEffect(() => {
if (!authed || (page !== "settings" && page !== "settingsNotify")) return;
@@ -487,19 +501,21 @@ 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 }) {
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[] }) {
// 异步生图:提交后立刻拿到任务列表,前端轮询直到出图。慢的 ARK 出图在 Celery worker 里跑——
// Web 层不被 ~30s 请求占住 → 健康探针不饿死 → 根治"几张图整站 502";且提交成功后浏览器关掉/断网,
// worker 仍会把图生成并落库(扣费/退费在 worker 内闭环),重开素材库即可见。
return action(async () => {
const { tasks } = await api.submitGenerateImage(payload);
const { tasks, conversation_id } = await api.submitGenerateImage(payload);
const ids = tasks.map((t) => t.id);
if (ids.length === 0) throw new Error("未能提交生成任务");
// 提交成功即落盘:刷新页面也能恢复"生成中"并继续轮询(任务在 worker 里跑,关掉浏览器也不丢)
// 记下本批所属商品(id+名),恢复在途批次时用它当导航头,而不是用「当前选中商品」(切走再回会显示错名)
const batchProduct = products.find((p) => p.id === payload.product_id);
saveImgwb(payload.mode, { pending: ids, results: [], count: payload.count, productId: payload.product_id, productTitle: batchProduct?.title });
return pollImageTasks(payload.mode, ids);
const res = await pollImageTasks(payload.mode, ids);
// 回传后端归属/新建的对话 id,供工作室把它登记进左栏会话列表并设为 active
return { ...res, conversation_id };
}, "图片已生成");
}
@@ -579,23 +595,29 @@ export function App() {
return assets[0].id;
}
async function onAuthed(payload: { token: string; user: User; team: Team; remember?: boolean }) {
async function onAuthed(payload: { token: string; user: User; team: Team; role?: string; remember?: boolean }) {
setToken(payload.token, payload.remember ?? true);
setUser(payload.user);
setTeam(payload.team);
setRole(payload.role || "");
setBooting(false);
setAuthed(true);
// 平台超管且无团队:直落后台,不拉团队级数据(否则 products/projects 等接口因无团队报错)
if (payload.user.is_platform_admin && !payload.team) {
setAuthed(true);
navigateAdmin("", { replace: true });
return;
}
navigate("dashboard", { replace: true });
// 首登水合:与 boot 路径一致地重试,失败不再静默(否则页面卡在全 0,要刷新才好)
loadDataWithRetry().catch((error) => {
// 先把工作台必需数据拉好,这期间「登录页」保持「登录成功,正在进入工作台…」提示(authed 仍 false → AuthScreen 不卸载),
// 数据就绪后才揭开外壳直接进有数据的工作台 —— 避免中间闪一个空白「加载中…」页(PMC#7)。
try {
await loadDataWithRetry();
} catch (error) {
console.error("[login] data hydrate failed:", error);
setNotice({ type: "error", text: "数据加载失败,请刷新页面重试" });
});
} finally {
setAuthed(true);
}
}
async function logout() {
@@ -604,6 +626,7 @@ export function App() {
setAuthed(false);
setUser(null);
setTeam(null);
setRole("");
setAuthMode("login");
window.history.replaceState(null, "", "/login");
}
@@ -643,7 +666,7 @@ export function App() {
<div className="content">
<div className="page-head">
<div>
<h1></h1>
<h1></h1>
<div className="sub">
<span className="mono">// 正在拉取团队数据</span>
</div>
@@ -992,7 +1015,7 @@ export function App() {
return (
<div className="app">
<Sidebar page={page} navigate={navigate} user={currentUser} team={currentTeam} products={products} projects={projects} productTotal={productTotal} projectTotal={projectTotal} logout={logout} onOpenAdmin={() => navigateAdmin("")} />
<Sidebar page={page} navigate={navigate} user={currentUser} team={currentTeam} canManageBilling={isOwner} products={products} projects={projects} productTotal={productTotal} projectTotal={projectTotal} logout={logout} onOpenAdmin={() => navigateAdmin("")} />
<main>
<Decorations />
<header className="topbar">