生图模型可选(火山/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:
+33
-10
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user