fix: update detail replacement and UI behavior

This commit is contained in:
Your Name 2026-06-02 14:10:08 +08:00
parent 37f6ef7e8e
commit 537a9d10c2
10 changed files with 1288 additions and 225 deletions

View File

@ -174,6 +174,18 @@ export async function normalizeReplacementResultImage(params: {
export type AgentReplacementModePayload = NonNullable<PostRequestBody["agentModePayload"]>;
export type AgentModeSourceImagePayload = NonNullable<AgentReplacementModePayload["sourceImages"]>;
export type AgentDetailReplacementStrategy = "scene_replace" | "product_only";
export type DetailReplacementGenerationRoute =
| "prompt_pack_scene"
| "layout_preserve_replace"
| "copy_locked_regenerate"
| "people_relation_locked"
| "direct_passthrough";
export type DetailReplacementPromptPackPolicy = "allow" | "soft" | "forbid";
export type DetailReplacementTextPolicy = "free" | "preserve" | "lock";
export type DetailReplacementPeoplePolicy =
| "none"
| "preserve_relation"
| "require_user_model";
export type NormalizedAgentModeSourceImage = {
url: string;
@ -196,6 +208,13 @@ export type ReplacementReferenceDecision = {
};
export type ReplacementScenePlan = {
generationRoute: DetailReplacementGenerationRoute;
promptPackPolicy: DetailReplacementPromptPackPolicy;
textPolicy: DetailReplacementTextPolicy;
peoplePolicy: DetailReplacementPeoplePolicy;
whiteBackgroundLikely: boolean;
copyImageCoupled: boolean;
routeReason: string;
useModelImage: boolean;
modelImageReason: string;
sceneSummary: string;
@ -240,11 +259,196 @@ export function truncateReplacementAnalysisText(value: unknown, maxLength = 240)
return String(value || "").trim().replace(/\s+/g, " ").slice(0, maxLength);
}
function normalizeDetailGenerationRoute(
value: unknown,
): DetailReplacementGenerationRoute | null {
const normalized = String(value || "")
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
if (!normalized) {
return null;
}
if (
normalized === "prompt_pack_scene" ||
normalized === "scene" ||
normalized === "prompt_scene" ||
normalized === "use_prompt_pack"
) {
return "prompt_pack_scene";
}
if (
normalized === "layout_preserve_replace" ||
normalized === "layout_preserve" ||
normalized === "preserve_layout" ||
normalized === "product_replace"
) {
return "layout_preserve_replace";
}
if (
normalized === "copy_locked_regenerate" ||
normalized === "copy_locked" ||
normalized === "text_locked" ||
normalized === "lock_copy"
) {
return "copy_locked_regenerate";
}
if (
normalized === "people_relation_locked" ||
normalized === "people_locked" ||
normalized === "model_relation_locked" ||
normalized === "preserve_people"
) {
return "people_relation_locked";
}
if (
normalized === "direct_passthrough" ||
normalized === "passthrough" ||
normalized === "keep_original" ||
normalized === "skip"
) {
return "direct_passthrough";
}
return null;
}
function normalizePromptPackPolicy(
value: unknown,
): DetailReplacementPromptPackPolicy | null {
if (typeof value === "boolean") {
return value ? "allow" : "forbid";
}
const normalized = String(value || "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (["allow", "allowed", "true", "yes", "use", "enable", "启用", "使用", "允许"].includes(normalized)) {
return "allow";
}
if (["soft", "weak", "limited", "reference", "参考", "弱化", "柔性"].includes(normalized)) {
return "soft";
}
if (["forbid", "forbidden", "false", "no", "disable", "禁用", "禁止", "不使用"].includes(normalized)) {
return "forbid";
}
return null;
}
function normalizeTextPolicy(value: unknown): DetailReplacementTextPolicy | null {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (["free", "rewrite", "自由", "可改写"].includes(normalized)) {
return "free";
}
if (["preserve", "keep", "保留", "保持"].includes(normalized)) {
return "preserve";
}
if (["lock", "locked", "copy_locked", "锁定", "锁文案", "严格保留"].includes(normalized)) {
return "lock";
}
return null;
}
function normalizePeoplePolicy(value: unknown): DetailReplacementPeoplePolicy | null {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (["none", "no_people", "无", "无人"].includes(normalized)) {
return "none";
}
if (
[
"preserve_relation",
"preserve",
"relation_locked",
"keep_people",
"保留关系",
"锁定人物关系",
].includes(normalized)
) {
return "preserve_relation";
}
if (
[
"require_user_model",
"require_model",
"user_model_only",
"需要用户模特",
"必须用户模特",
].includes(normalized)
) {
return "require_user_model";
}
return null;
}
function inferDetailGenerationRoute(params: {
shouldReplace: boolean;
isProductOnly: boolean;
whiteBackgroundLikely: boolean;
copyImageCoupled: boolean;
hasImportantText: boolean;
containsPeople: boolean;
containsMultiplePeople: boolean;
}): DetailReplacementGenerationRoute {
if (!params.shouldReplace) {
return "direct_passthrough";
}
if (params.isProductOnly) {
return "layout_preserve_replace";
}
if (params.whiteBackgroundLikely) {
return "layout_preserve_replace";
}
if (params.copyImageCoupled || params.hasImportantText) {
return "copy_locked_regenerate";
}
if (params.containsMultiplePeople || params.containsPeople) {
return "people_relation_locked";
}
return "prompt_pack_scene";
}
function inferPromptPackPolicy(
route: DetailReplacementGenerationRoute,
): DetailReplacementPromptPackPolicy {
return route === "prompt_pack_scene" ? "allow" : "forbid";
}
function inferTextPolicy(params: {
route: DetailReplacementGenerationRoute;
hasImportantText: boolean;
copyImageCoupled: boolean;
}): DetailReplacementTextPolicy {
if (params.route === "copy_locked_regenerate" || params.copyImageCoupled) {
return "lock";
}
if (params.hasImportantText || params.route === "layout_preserve_replace") {
return "preserve";
}
return "free";
}
function inferPeoplePolicy(params: {
containsPeople: boolean;
containsMultiplePeople: boolean;
hasModelImage: boolean;
}): DetailReplacementPeoplePolicy {
if (!params.containsPeople && !params.containsMultiplePeople) {
return "none";
}
return params.hasModelImage ? "preserve_relation" : "require_user_model";
}
export function parseReplacementScenePlan(
text: string,
params: {
productImageCount: number;
hasModelImage: boolean;
replacementStrategy?: AgentDetailReplacementStrategy;
},
): ReplacementScenePlan | null {
const jsonText = extractJsonObjectText(text);
@ -254,6 +458,69 @@ export function parseReplacementScenePlan(
try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>;
const isProductOnly = params.replacementStrategy === "product_only";
const shouldReplace =
normalizeBooleanLike(parsed.shouldReplace) ??
normalizeBooleanLike(parsed.replace) ??
normalizeBooleanLike(parsed.isReplaceable) ??
true;
const whiteBackgroundLikely =
normalizeBooleanLike(parsed.whiteBackgroundLikely) ??
normalizeBooleanLike(parsed.whiteBackground) ??
normalizeBooleanLike(parsed.isWhiteBackground) ??
false;
const copyImageCoupled =
normalizeBooleanLike(parsed.copyImageCoupled) ??
normalizeBooleanLike(parsed.textImageCoupled) ??
normalizeBooleanLike(parsed.copyMatchesImage) ??
false;
const hasImportantText =
normalizeBooleanLike(parsed.hasImportantText) ??
normalizeBooleanLike(parsed.textHeavy) ??
normalizeBooleanLike(parsed.containsMarketingCopy) ??
normalizeBooleanLike(parsed.containsImportantCopy) ??
false;
const containsPeople =
normalizeBooleanLike(parsed.containsPeople) ??
normalizeBooleanLike(parsed.hasPeople) ??
normalizeBooleanLike(parsed.peoplePresent) ??
false;
const containsMultiplePeople =
normalizeBooleanLike(parsed.containsMultiplePeople) ??
normalizeBooleanLike(parsed.multiPerson) ??
normalizeBooleanLike(parsed.multiplePeople) ??
false;
const generationRoute =
normalizeDetailGenerationRoute(
parsed.generationRoute || parsed.route || parsed.recommendedRoute,
) ||
inferDetailGenerationRoute({
shouldReplace,
isProductOnly,
whiteBackgroundLikely,
copyImageCoupled,
hasImportantText,
containsPeople,
containsMultiplePeople,
});
const promptPackPolicy =
normalizePromptPackPolicy(
parsed.promptPackPolicy ?? parsed.usePromptPack ?? parsed.allowPromptPack,
) || inferPromptPackPolicy(generationRoute);
const textPolicy =
normalizeTextPolicy(parsed.textPolicy || parsed.copyPolicy) ||
inferTextPolicy({
route: generationRoute,
hasImportantText,
copyImageCoupled,
});
const peoplePolicy =
normalizePeoplePolicy(parsed.peoplePolicy || parsed.modelPolicy) ||
inferPeoplePolicy({
containsPeople,
containsMultiplePeople,
hasModelImage: params.hasModelImage,
});
const useModelImageRaw =
normalizeBooleanLike(parsed.useModelImage) ??
normalizeBooleanLike(parsed.needModelImage) ??
@ -266,6 +533,16 @@ export function parseReplacementScenePlan(
);
const scenePlan: ReplacementScenePlan = {
generationRoute,
promptPackPolicy,
textPolicy,
peoplePolicy,
whiteBackgroundLikely,
copyImageCoupled,
routeReason: truncateReplacementAnalysisText(
parsed.routeReason || parsed.strategyReason || parsed.policyReason || parsed.reason,
180,
),
useModelImage: params.hasModelImage ? Boolean(useModelImageRaw) : false,
modelImageReason: truncateReplacementAnalysisText(
parsed.modelImageReason || parsed.modelReason,
@ -310,6 +587,7 @@ export function parseReplacementScenePlan(
if (!scenePlan.generationPrompt) {
scenePlan.generationPrompt = [
scenePlan.routeReason,
scenePlan.sceneSummary,
scenePlan.cameraSummary,
scenePlan.placementSummary,
@ -335,7 +613,23 @@ export function buildHeuristicReplacementScenePlan(params: {
replacementStrategy?: AgentDetailReplacementStrategy;
}): ReplacementScenePlan {
const isProductOnly = params.replacementStrategy === "product_only";
const generationRoute = params.shouldReplace
? isProductOnly
? "layout_preserve_replace"
: "prompt_pack_scene"
: "direct_passthrough";
return {
generationRoute,
promptPackPolicy: inferPromptPackPolicy(generationRoute),
textPolicy: generationRoute === "direct_passthrough" ? "preserve" : "free",
peoplePolicy: "none",
whiteBackgroundLikely: false,
copyImageCoupled: false,
routeReason: params.shouldReplace
? isProductOnly
? "兜底按仅替换产品执行,不使用词包。"
: "兜底未检测到强约束,允许使用词包生成场景。"
: params.reason,
selectedProductImageIndexes: Array.from(
{ length: Math.min(Math.max(params.productImageCount, 1), 2) },
(_, index) => index + 1,
@ -845,7 +1139,7 @@ export async function analyzeReplacementExecutionPlan(params: {
isProductOnlyDetailReplacement
? "这张参考图要求最大程度原样保留。后续生图只允许替换其中的产品主体/产品展示区域,背景、文案、按钮、图标、分割线、品牌标识、留白、装饰和光影氛围都必须保留。"
: isDetailReplacement
? "这张参考图不是要求原样保留的场景照片。后续生图只允许替换/重建背景图、产品场景图、商品主体图或人物展示图区域;文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版必须保留。"
? "这张参考图可能是场景图、白底说明图、强文案图、人物关系图或纯版式图。你必须先判断它适合哪一种执行路由,再决定是否允许前置工作流词包参与。文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版必须保留。"
: "这张参考图不是要求原样保留的场景照片。原图里的沙发、床、柜体、墙面、地面、窗帘、灯具、摆件、桌面道具、背景家具和环境光氛围,都可以在后续生图阶段按前置工作流风格重建。",
"请完成以下任务:",
isProductOnlyDetailReplacement
@ -854,27 +1148,47 @@ export async function analyzeReplacementExecutionPlan(params: {
? "1. 判断 shouldReplace:当前参考图里是否存在适合被替换/重建的背景图、产品场景图、商品主体图、人物展示图或大面积视觉图片区。纯文字/纯参数/纯品牌介绍且没有视觉图片区时才为 false。"
: "1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被替换的家具商品主体。",
"2. 判断 useModelImage,并说明原因。",
"3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。",
isDetailReplacement
? "3. 判断 generationRoute:prompt_pack_scene=通用场景图且可吃词包;layout_preserve_replace=锁定版式/白底/产品说明,只做有限替换;copy_locked_regenerate=文案与画面强绑定,必须锁文案;people_relation_locked=多人/模特/互动关系图,必须锁人物关系;direct_passthrough=纯文字/白底说明/无可替换视觉区,直接沿用。"
: "3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。",
isDetailReplacement
? "4. 判断 promptPackPolicy:只有通用空间场景且没有强文案、白底、多人关系、词图强绑定限制时才 allow;白底图、强文案图、多人关系图、图文匹配强的详情页必须 forbid。"
: "",
isDetailReplacement
? "5. 判断 textPolicy:画面中存在营销标题、卖点、编号、参数、标签且与图匹配时必须 lock 或 preserve,避免后续生图改字、混色或重排。"
: "",
isDetailReplacement
? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/模特互动时必须 preserve_relation;如果没有提供用户模特图但参考图有人物身份,不允许把参考人物身份当成用户模特直接迁移。"
: "",
isDetailReplacement
? "7. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。"
: "",
isProductOnlyDetailReplacement
? "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,要求只替换产品主体,保留其余背景、文案、排版、装饰和光影完全不变。"
: isDetailReplacement
? "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述需要替换的背景/场景图片区、产品放置方式、模特使用规则,以及必须保留的文字/UI/图标/排版。"
? "8. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令。若 promptPackPolicy=forbid,必须明确“不使用前置词包,参考图语义优先”;若 textPolicy=lock,必须明确文字内容、颜色、层级和位置不可改;若 peoplePolicy=preserve_relation,必须明确人物数量、关系、动作和身份约束。"
: "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述这一屏需要的场景、机位、主体位置、周边环境重建方向以及必须保留的内容。",
isProductOnlyDetailReplacement
? "5. sceneSummary、styleSummary 和 generationPrompt 必须强调‘只替换产品,不动其他内容’,不要引导重建场景。"
: isDetailReplacement
? "9. 如果 generationRoute 不是 prompt_pack_scene,sceneSummary、styleSummary 和 generationPrompt 必须优先服务参考图原有语义、版式、文案和人物关系,不要强行套前置词包。"
: "5. sceneSummary、styleSummary 和 generationPrompt 必须优先描述“按前置工作流重建后的目标场景”,而不是只复述参考图当前已有场景。",
isProductOnlyDetailReplacement
? "6. avoidSummary 里要明确避免改背景、改文字、改按钮、改图标、改版式、改留白、改装饰、改光影氛围。"
: isDetailReplacement
? "10. 如果前置工作流风格与当前参考图原场景冲突,只有 promptPackPolicy=allow 时才以前置词包为主;其他情况必须以参考图语义和版式为主。"
: "6. 如果前置工作流风格与当前参考图原场景冲突,必须保留版式骨架,但放弃原场景物件本身,以前置工作流指定的空间风格、软装、灯光和氛围为准。",
isProductOnlyDetailReplacement
? "7. 不要根据用户提示词或工作流风格改造场景,只服务于产品主体替换。"
: isDetailReplacement
? "11. avoidSummary 必须包含该路由下最容易失败的风险,例如:词图不匹配、文案变色、人物换脸、白底图被场景化、多人关系丢失。"
: "7. avoidSummary 里要明确避免“只替换产品、不重建场景”这类结果。",
workflowPromptExcerpt
? `当前风格与场景限制(来自前置工作流与模式模板):\n${workflowPromptExcerpt}`
: "",
"只返回严格 JSON,不要任何额外解释。",
`JSON 格式:
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`,
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","generationRoute":"prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked|direct_passthrough","promptPackPolicy":"allow|soft|forbid","textPolicy":"free|preserve|lock","peoplePolicy":"none|preserve_relation|require_user_model","whiteBackgroundLikely":false,"copyImageCoupled":false,"containsPeople":false,"containsMultiplePeople":false,"routeReason":"不超过50字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`,
]
.filter(Boolean)
.join("\n");
@ -909,6 +1223,7 @@ export async function analyzeReplacementExecutionPlan(params: {
hasModelImage: isProductOnlyDetailReplacement
? false
: Boolean(params.modelImageUrl),
replacementStrategy: params.replacementStrategy,
});
if (parsedDecision && parsedScenePlan) {
@ -958,6 +1273,31 @@ export async function analyzeReplacementExecutionPlan(params: {
};
}
function shouldApplyPromptPackToDetailReplacement(params: {
replacementStrategy?: AgentDetailReplacementStrategy;
scenePlan?: ReplacementScenePlan | null;
}) {
if (params.replacementStrategy === "product_only") {
return false;
}
const scenePlan = params.scenePlan || null;
if (!scenePlan) {
return true;
}
if (scenePlan.promptPackPolicy === "forbid") {
return false;
}
if (
scenePlan.generationRoute === "direct_passthrough" ||
scenePlan.generationRoute === "layout_preserve_replace" ||
scenePlan.generationRoute === "copy_locked_regenerate" ||
scenePlan.generationRoute === "people_relation_locked"
) {
return scenePlan.promptPackPolicy === "allow";
}
return true;
}
export function buildAgentReplacementPrompt(params: {
basePrompt: string;
modeType: "detail_replacement" | "sku_replacement";
@ -975,6 +1315,12 @@ export function buildAgentReplacementPrompt(params: {
}) {
const imageOrderLines: string[] = [];
let nextImageIndex = 1;
const shouldApplyPromptPack =
params.modeType !== "detail_replacement" ||
shouldApplyPromptPackToDetailReplacement({
replacementStrategy: params.replacementStrategy,
scenePlan: params.scenePlan,
});
if (params.productImageCount > 0) {
const productRangeLabel =
@ -1012,6 +1358,19 @@ export function buildAgentReplacementPrompt(params: {
const scenePlanLines = params.scenePlan
? [
"【本屏场景预分析】",
`自动路由:${params.scenePlan.generationRoute}`,
`词包策略:${params.scenePlan.promptPackPolicy}`,
`文案策略:${params.scenePlan.textPolicy}`,
`人物策略:${params.scenePlan.peoplePolicy}`,
params.scenePlan.routeReason
? `路由理由:${params.scenePlan.routeReason}`
: "",
params.scenePlan.whiteBackgroundLikely
? "白底/说明图判断:是,禁止强行场景化。"
: "",
params.scenePlan.copyImageCoupled
? "图文绑定判断:是,文案与画面语义必须保持匹配。"
: "",
params.scenePlan.sceneSummary
? `场景理解:${params.scenePlan.sceneSummary}`
: "",
@ -1082,29 +1441,60 @@ export function buildAgentReplacementPrompt(params: {
...imageOrderLines,
...scenePlanLines,
"你现在做的是“详情参考图背景/场景图替换”,不是重新设计整张详情图。",
"要把最后一张参考图理解为“页面版式底板 + 背景图/场景图区参考”,不是“旧场景照片必须原样保留”。",
"前面“工作流设定”里的场景组合、空间风格、硬装方案、软装方案、灯光氛围、人群设定,必须作为当前这一屏的强制场景改造要求生效。",
"场景风格必须优先执行前面用户提示词与工作流设定中给出的空间风格、材质、灯光和氛围描述,不要被参考图原有旧场景绑死。",
"执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。",
shouldApplyPromptPack
? "要把最后一张参考图理解为“页面版式底板 + 背景图/场景图区参考”,不是“旧场景照片必须原样保留”。"
: "本屏已自动判定为“参考图语义优先”,不要把最后一张参考图当成可随意重写的场景模板。",
shouldApplyPromptPack
? "前面“工作流设定”里的场景组合、空间风格、硬装方案、软装方案、灯光氛围、人群设定,必须作为当前这一屏的强制场景改造要求生效。"
: "本屏不使用前置工作流词包;如果词包里的空间、人群、灯光、风格与参考图冲突,必须忽略词包,以参考图当前图文语义为准。",
shouldApplyPromptPack
? "场景风格必须优先执行前面用户提示词与工作流设定中给出的空间风格、材质、灯光和氛围描述,不要被参考图原有旧场景绑死。"
: "不要把厨房、灯光、人群、主题风格等前置选择强行套入本屏;本屏只做必要的产品/视觉区替换和自然融合。",
shouldApplyPromptPack
? "执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。"
: "执行时必须保持参考图原有画面主题、文案语义、人物关系、构图和版式,只在可替换视觉区做有限替换。",
"允许替换/重建的范围是参考图里的背景图、产品场景图、商品主体图和必要的人物展示图区;不允许改动版式层。",
"如果前面商品图、模特图与最后一张参考图底板存在冲突,始终以最后一张参考图底板的版式、机位、构图和信息层级为最高优先级。",
"最后一张参考图是唯一底板,必须完整保留这张图的文字排版、品牌字样、标题区、卖点区、UI图标、按钮、标签、装饰元素、分割线、留白、画布比例和信息层级。",
params.scenePlan?.textPolicy === "lock"
? "本屏文案已判定为强锁定:标题、卖点、参数、编号、标签文字的内容、颜色层级、位置和排版都不得改写、变色或拆成多种颜色。"
: "如果参考图里存在文字或标签,必须保持文字区位置和信息层级,不要擅自改写或重排。",
params.scenePlan?.peoplePolicy === "require_user_model"
? "本屏包含人物但没有提供用户模特图:禁止把参考图中的人物身份当作用户模特迁移;只能保留人物数量、关系、动作和构图语义,不要换成不相关强人物主体。"
: params.scenePlan?.peoplePolicy === "preserve_relation"
? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例,不要把多人图改成单人图或无关模特图。"
: "如果参考图没有明确人物展示需求,不要凭空新增人物主体。",
params.scenePlan?.whiteBackgroundLikely
? "本屏疑似白底/说明图:禁止场景化、生活化或套空间词包,只允许保留白底/说明图语义并做必要的局部替换。"
: "",
params.referenceGeometry
? `当前参考图画布尺寸为 ${params.referenceGeometry.width}x${params.referenceGeometry.height},输出必须保持这个页面画布比例。替换背景/商品时只能等比缩放后裁切或延展背景,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。`
: "输出必须保持当前参考图页面画布比例。替换背景/商品时只能等比缩放后裁切或延展背景,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。",
"镜头角度、透视关系、视平线、主体朝向和主体占比必须尽量跟随最后一张参考图,方便无缝替换当前版式,不要擅自改成完全不同的视角。",
"前面的商品图和模特图用于生成新的背景图/产品场景图内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。",
"可被替换重建的内容包括但不限于:背景照片、产品展示照片、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。",
shouldApplyPromptPack
? "前面的商品图和模特图用于生成新的背景图/产品场景图内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。"
: "前面的商品图用于必要的产品/视觉区替换;模特图只在本屏明确需要人物且策略允许时使用,不得覆盖参考图原有语义、版式和图文关系。",
shouldApplyPromptPack
? "可被替换重建的内容包括但不限于:背景照片、产品展示照片、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。"
: "可被替换的内容仅限参考图里明确的产品/视觉图片区;不要新增不属于参考图语义的空间、人物、道具、主题和文案。",
params.productImageCount > 1
? `前面的 ${params.productImageCount} 张商品输入图都属于同一产品的多角度/多细节白底图,请综合这些图片理解产品结构、材质、颜色、比例、边角、开孔、五金与细节,不可只参考第一张。`
: "前面的商品输入图就是当前要替换进去的产品白底图,请准确保持该产品的结构、材质、颜色、比例和细节。",
"请先识别最后一张参考图中需要被替换的背景图/场景图区域,并在同一版式骨架下生成新的背景画面;不要只做局部抠图式换产品。",
"请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置和信息层级;但旧背景图里的家具、背景物件、材质、灯光氛围、软装小物、空间质感需要按第一步工作流设定进行明显重建。",
"如果第一步工作流风格与参考图原场景冲突,必须保留版式骨架和UI层,但放弃原背景图物件本身,以第一步工作流风格为准。",
shouldApplyPromptPack
? "请先识别最后一张参考图中需要被替换的背景图/场景图区域,并在同一版式骨架下生成新的背景画面;不要只做局部抠图式换产品。"
: "请先识别最后一张参考图中真正允许替换的视觉区;如果只是白底说明、强文案页、多人关系图或图文匹配强的内容,不要强行重建整张场景。",
shouldApplyPromptPack
? "请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置和信息层级;但旧背景图里的家具、背景物件、材质、灯光氛围、软装小物、空间质感需要按第一步工作流设定进行明显重建。"
: "请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置、人物关系和信息层级;不要因为前置词包改变这些内容。",
shouldApplyPromptPack
? "如果第一步工作流风格与参考图原场景冲突,必须保留版式骨架和UI层,但放弃原背景图物件本身,以第一步工作流风格为准。"
: "如果第一步工作流风格与参考图原场景冲突,必须保留参考图语义与版式,不要放弃原有图文关系。",
"前面的输入商品图用于提供商品主体、材质、颜色和细节特征;如有模特图,请只在参考图本身包含人物展示时再参考模特图。",
"如果提供了模特图且这一屏确实有人物展示需求,必须尽量复用同一人物身份与体态关系,不要明显换脸、换发型、换体型,服装风格也尽量保持一致。",
"绝对不要忽略最后一张参考图去生成一张无关的新场景图。",
"如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。",
shouldApplyPromptPack
? "如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。"
: "如果本屏已禁用词包,生成结果不应追求明显改造背景,而应追求图文匹配、文案稳定、人物关系稳定和局部替换自然。",
"绝对不要移动、删除、改写、重排参考图里的任何文字、UI图标、按钮、标签、装饰元素与排版元素。",
"输出结果必须保持最后一张参考图底板的原始纵横比与画布尺寸,不允许擅自改成长图、横图或裁掉任何版面区域。",
"如果参考图中没有可替换/重建的背景图、产品场景图或视觉图片区,应直接沿用参考图,不要硬生成。",
@ -1133,17 +1523,16 @@ export function buildAgentReplacementPrompt(params: {
];
const instructionLines =
params.modeType === "detail_replacement"
(params.modeType === "detail_replacement"
? productDetailInstructionLines
: skuInstructionLines;
: skuInstructionLines).filter(Boolean);
if (params.hasModelImage) {
instructionLines.push("如有模特图,请在需要人物展示时保持模特体态、穿搭和出镜关系一致。");
}
return [
params.modeType === "detail_replacement" &&
params.replacementStrategy === "product_only"
params.modeType === "detail_replacement" && !shouldApplyPromptPack
? ""
: params.basePrompt,
instructionLines.join("\n"),

View File

@ -3398,6 +3398,19 @@ export async function POST(request: Request) {
planningRuntime: replacementScenePlanningRuntime,
models: replacementVisionModels,
});
chatApiLogger.info("[Chat API] Direct detail replacement reference decision:", {
referenceIndex: referenceIndex + 1,
totalReferences: replacementReferences.length,
referenceLabel: reference.label,
shouldReplace: replacementExecutionPlan.shouldReplace,
reason: replacementExecutionPlan.reason,
detectionSource: replacementExecutionPlan.detectionSource,
scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
textPolicy: replacementExecutionPlan.scenePlan.textPolicy,
peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy,
});
const referenceSourceImageUrls = Array.from(
new Set([
...productInputImageUrls,
@ -4117,6 +4130,10 @@ export async function POST(request: Request) {
detectionModelName: replacementExecutionPlan.detectionModelName,
detectionSource: replacementExecutionPlan.detectionSource,
scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
textPolicy: replacementExecutionPlan.scenePlan.textPolicy,
peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy,
selectedProductImageIndexes:
replacementExecutionPlan.scenePlan.selectedProductImageIndexes,
useModelImage: replacementExecutionPlan.scenePlan.useModelImage,
@ -4133,6 +4150,9 @@ export async function POST(request: Request) {
replacementExecutionPlan.scenePlan.productSelectionReason,
useModelImage: replacementExecutionPlan.scenePlan.useModelImage,
modelImageReason: replacementExecutionPlan.scenePlan.modelImageReason,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
routeReason: replacementExecutionPlan.scenePlan.routeReason,
});
const referenceSourceImageUrls = Array.from(
new Set([

View File

@ -773,28 +773,6 @@ function buildReferenceCardsFromCandidate(params: {
}));
}
function readLocalImageDimensions(file: File): Promise<{ width: number | null; height: number | null }> {
if (typeof window === "undefined") {
return Promise.resolve({ width: null, height: null });
}
return new Promise((resolve) => {
const objectUrl = URL.createObjectURL(file);
const image = new Image();
const finish = (width: number | null, height: number | null) => {
URL.revokeObjectURL(objectUrl);
resolve({ width, height });
};
image.onload = () => {
finish(image.naturalWidth || null, image.naturalHeight || null);
};
image.onerror = () => {
finish(null, null);
};
image.src = objectUrl;
});
}
function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) {
const rawImageCount =
target === "main"
@ -880,6 +858,19 @@ function getCandidateTargetCount(
return rawCount;
}
const IMAGE_OVERLAY_BADGE_CLASS =
"rounded-full border border-white/25 bg-zinc-950/82 px-3 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md ring-1 ring-black/20";
const IMAGE_OVERLAY_ACTIVE_BADGE_CLASS =
"rounded-full border border-emerald-100/80 bg-emerald-600/92 px-3 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(16,185,129,0.30),0_10px_28px_rgba(0,0,0,0.48)] backdrop-blur-md";
const IMAGE_OVERLAY_STATUS_BASE_CLASS =
"rounded-full border px-3 py-1.5 font-semibold text-[11px] shadow-[0_12px_34px_rgba(0,0,0,0.50)] backdrop-blur-md ring-1 ring-black/20";
const IMAGE_OVERLAY_ACTION_CLASS =
"rounded-full border border-white/25 bg-zinc-950/82 px-3.5 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md transition-colors hover:border-emerald-200/70 hover:bg-zinc-900/92";
const IMAGE_OVERLAY_DANGER_TEXT_CLASS =
"rounded-full border border-rose-200/55 bg-zinc-950/82 px-3 py-1.5 font-semibold text-[11px] text-rose-50 shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md transition-colors hover:border-rose-100 hover:bg-rose-500/30";
const IMAGE_OVERLAY_DANGER_ICON_CLASS =
"absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-rose-200/55 bg-zinc-950/82 text-rose-50 shadow-[0_12px_34px_rgba(0,0,0,0.50)] backdrop-blur-md transition-colors hover:border-rose-100 hover:bg-rose-500/35";
type ReplacementCardStatus =
| "waiting"
| "queued"
@ -998,37 +989,37 @@ function getReplacementCardStatusMeta(status: ReplacementCardStatus) {
return {
label: "不处理直出",
className:
"border-cyan-200/70 bg-cyan-500/85 text-white shadow-[0_8px_24px_rgba(6,182,212,0.28)]",
"border-cyan-100/80 bg-cyan-600/92 text-white",
};
case "fallback":
return {
label: "失败已沿用",
className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200",
className: "border-amber-100/80 bg-amber-500/92 text-zinc-950",
};
case "completed":
return {
label: "已生成",
className: "border-emerald-400/30 bg-emerald-500/[0.10] text-emerald-200",
className: "border-emerald-100/80 bg-emerald-600/92 text-white",
};
case "generating":
return {
label: "生成中",
className: "border-sky-400/30 bg-sky-500/[0.10] text-sky-200",
className: "border-sky-100/80 bg-sky-600/92 text-white",
};
case "queued":
return {
label: "排队中",
className: "border-white/12 bg-white/[0.05] text-white/60",
className: "border-white/25 bg-zinc-950/82 text-white",
};
case "missing":
return {
label: "未回传",
className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200",
className: "border-amber-100/80 bg-amber-500/92 text-zinc-950",
};
default:
return {
label: "待生成",
className: "border-white/12 bg-white/[0.05] text-white/60",
className: "border-white/25 bg-zinc-950/82 text-white",
};
}
}
@ -1331,14 +1322,6 @@ export function AgentGenerationWorkspaceDarkV2({
isLoading: false,
lastQuery: null,
});
const detailReferenceUploadInputRef = useRef<HTMLInputElement | null>(null);
const [detailReferenceUploadState, setDetailReferenceUploadState] = useState<{
isUploading: boolean;
count: number;
}>({
isUploading: false,
count: 0,
});
const [detailReferenceStitchDialogOpen, setDetailReferenceStitchDialogOpen] = useState(false);
const [activeDetailReferenceCardId, setActiveDetailReferenceCardId] = useState<string | null>(null);
const [skuReplacementSearchState, setSkuReplacementSearchState] = useState<{
@ -4868,108 +4851,6 @@ export function AgentGenerationWorkspaceDarkV2({
},
[updateDetailReplacementState]
);
const handleDetailReferenceUploadChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const inputElement = event.currentTarget;
const rawFiles = Array.from(inputElement.files || []);
const imageFiles = rawFiles.filter((file) => file.type.startsWith("image/"));
if (rawFiles.length > 0 && imageFiles.length === 0) {
toast.error("请上传图片文件");
inputElement.value = "";
return;
}
const availableSlotCount = Math.max(
MAX_AGENT_MODE_REFERENCE_COUNT - detailReplacementReferenceCards.length,
0
);
if (availableSlotCount <= 0) {
toast.error(`最多支持 ${MAX_AGENT_MODE_REFERENCE_COUNT} 张参考图,请先删除部分图片`);
inputElement.value = "";
return;
}
const uploadFiles = imageFiles.slice(0, availableSlotCount);
if (imageFiles.length > uploadFiles.length) {
toast.warning(`已达到参考图上限,本次只上传前 ${uploadFiles.length} 张`);
}
if (uploadFiles.length === 0) {
inputElement.value = "";
return;
}
setDetailReferenceUploadState({
isUploading: true,
count: uploadFiles.length,
});
try {
const uploadedItems = await Promise.all(
uploadFiles.map(async (file, index) => {
const dimensions = await readLocalImageDimensions(file);
const formData = new FormData();
formData.append("file", file);
const response = await fetchUploadWithRetry(formData);
const data = await response.json().catch(() => ({}));
if (!response.ok || !data?.url) {
throw new Error(data?.error || `${file.name || `第 ${index + 1} 张`}上传失败`);
}
return {
index,
file,
url: String(data.url),
previewUrl: String(data.previewUrl || data.thumbnailUrl || data.url || ""),
width: dimensions.width,
height: dimensions.height,
};
})
);
const timestamp = Date.now();
const orderedItems = uploadedItems.slice().sort((left, right) => left.index - right.index);
updateDetailReplacementState((current) => {
const baseLength = current.references.length;
const nextReferences = [
...current.references,
...orderedItems.map((item, index) => ({
id: `detail-upload-${timestamp}-${index + 1}-${generateUUID().slice(0, 8)}`,
label: item.file.name
? `上传详情图 ${baseLength + index + 1} · ${item.file.name}`
: `上传详情图 ${baseLength + index + 1}`,
imageUrl: item.url,
previewUrl: item.previewUrl || item.url,
width: item.width,
height: item.height,
candidateId: null,
sortOrder: baseLength + index,
passthrough: false,
assignedProductIndex: null,
})),
]
.slice(0, MAX_AGENT_MODE_REFERENCE_COUNT)
.map((card, index) => ({
...card,
sortOrder: index,
}));
return {
...current,
references: nextReferences,
};
});
toast.success(`已按文件选择顺序上传 ${orderedItems.length} 张参考详情图`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "上传参考详情图失败");
} finally {
setDetailReferenceUploadState({
isUploading: false,
count: 0,
});
inputElement.value = "";
}
},
[detailReplacementReferenceCards.length, updateDetailReplacementState]
);
const handleDetailReplacementSearch = useCallback(async (
queryType: Extract<AgentWorkflowReferenceQueryType, "spu" | "productId">
) => {
@ -6249,14 +6130,6 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
<div className="space-y-4 p-5">
<input
ref={detailReferenceUploadInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={handleDetailReferenceUploadChange}
/>
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="min-w-0">
@ -6321,27 +6194,8 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</div>
<div className="flex flex-col gap-2 lg:flex-row">
<Button
type="button"
variant="outline"
onClick={() => detailReferenceUploadInputRef.current?.click()}
disabled={detailReferenceUploadState.isUploading}
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
>
{detailReferenceUploadState.isUploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
上传 {detailReferenceUploadState.count} 张
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
上传参考图
</>
)}
</Button>
{detailReplacementReferenceCards.length > 0 ? (
{detailReplacementReferenceCards.length > 0 ? (
<div className="flex flex-col gap-2 lg:flex-row">
<Button
type="button"
variant="outline"
@ -6350,8 +6204,6 @@ export function AgentGenerationWorkspaceDarkV2({
>
拼合切割参考图
</Button>
) : null}
{detailReplacementReferenceCards.length > 0 ? (
<Button
type="button"
variant="outline"
@ -6360,8 +6212,8 @@ export function AgentGenerationWorkspaceDarkV2({
>
清空参考图
</Button>
) : null}
</div>
</div>
) : null}
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
<div className="rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
@ -6826,7 +6678,7 @@ export function AgentGenerationWorkspaceDarkV2({
event.stopPropagation();
handleRemoveDetailReferenceCard(card.id);
}}
className="rounded-full border border-white/10 bg-black/40 px-2 py-1 text-[11px] text-white/70 transition-colors hover:border-white/20 hover:text-white"
className={IMAGE_OVERLAY_DANGER_TEXT_CLASS}
>
删除
</button>
@ -6837,10 +6689,10 @@ export function AgentGenerationWorkspaceDarkV2({
handleToggleDetailReferenceCardPassthrough(card.id);
}}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-[12px] font-semibold backdrop-blur-md shadow-[0_10px_28px_rgba(0,0,0,0.35)] transition-colors",
"inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 font-semibold text-[12px] backdrop-blur-md shadow-[0_12px_34px_rgba(0,0,0,0.50)] transition-colors",
card.passthrough
? "border-emerald-200 bg-emerald-500 text-white shadow-[0_10px_28px_rgba(16,185,129,0.36)]"
: "border-emerald-300/85 bg-black/72 text-emerald-50 hover:border-emerald-200 hover:bg-emerald-500/22"
? "border-emerald-100/85 bg-emerald-600/92 text-white shadow-[0_12px_34px_rgba(16,185,129,0.34),0_10px_28px_rgba(0,0,0,0.45)]"
: "border-emerald-100/70 bg-zinc-950/82 text-emerald-50 hover:border-emerald-50 hover:bg-emerald-700/80"
)}
>
<CheckCircle2 className="h-3.5 w-3.5" />
@ -6859,18 +6711,19 @@ export function AgentGenerationWorkspaceDarkV2({
}
/>
) : null}
<div className="absolute left-3 top-3 rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/78">
<div className={cn("absolute left-3 top-3", IMAGE_OVERLAY_BADGE_CLASS)}>
参考图 {index + 1}
</div>
{card.passthrough ? (
<div className="absolute left-3 top-12 rounded-full border border-cyan-200/70 bg-cyan-500/85 px-2.5 py-1 text-[11px] text-white shadow-[0_8px_24px_rgba(6,182,212,0.28)]">
<div className={cn("absolute left-3 top-12", IMAGE_OVERLAY_STATUS_BASE_CLASS, "border-cyan-100/80 bg-cyan-600/92 text-white")}>
不处理直出
</div>
) : null}
{isActive ? (
<div
className={cn(
"absolute rounded-full border border-emerald-300/35 bg-emerald-500/[0.14] px-2.5 py-1 text-[11px] text-emerald-100",
"absolute",
IMAGE_OVERLAY_ACTIVE_BADGE_CLASS,
card.passthrough ? "left-3 top-[3.75rem]" : "left-3 top-12"
)}
>
@ -6955,11 +6808,11 @@ export function AgentGenerationWorkspaceDarkV2({
setFullscreenPreview(card.resultUrl!);
}}
/>
<div className={cn("absolute left-3 top-3 rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
<div className={cn("absolute left-3 top-3", IMAGE_OVERLAY_STATUS_BASE_CLASS, statusMeta.className)}>
{statusMeta.label}
</div>
{isActive ? (
<div className="absolute left-3 top-12 rounded-full border border-emerald-300/35 bg-emerald-500/[0.14] px-2.5 py-1 text-[11px] text-emerald-100">
<div className={cn("absolute left-3 top-12", IMAGE_OVERLAY_ACTIVE_BADGE_CLASS)}>
当前联动
</div>
) : null}
@ -6968,7 +6821,7 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={() =>
handleDeleteResultImage("detail_replacement", card.resultUrl!)
}
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
className={IMAGE_OVERLAY_DANGER_ICON_CLASS}
aria-label="删除结果图"
>
<Trash2 className="h-3.5 w-3.5" />
@ -6978,7 +6831,7 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={() => {
void handleDownloadOriginalImage(card.resultUrl!);
}}
className="absolute bottom-3 right-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute bottom-3 right-3", IMAGE_OVERLAY_ACTION_CLASS)}
>
下载
</button>
@ -6993,7 +6846,7 @@ export function AgentGenerationWorkspaceDarkV2({
title: `${selectedAgentPreset?.name || "参考图替换"} 结果图 ${card.index + 1}`,
})
}
className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={IMAGE_OVERLAY_ACTION_CLASS}
>
当前页改字
</button>
@ -7009,7 +6862,7 @@ export function AgentGenerationWorkspaceDarkV2({
});
setReplacementRerunPrompt("");
}}
className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20 disabled:cursor-not-allowed disabled:opacity-40"
className={cn(IMAGE_OVERLAY_ACTION_CLASS, "disabled:cursor-not-allowed disabled:opacity-45")}
>
重新生图
</button>
@ -7352,7 +7205,7 @@ export function AgentGenerationWorkspaceDarkV2({
<button
type="button"
onClick={() => handleRemoveSkuReferenceCard(card.id)}
className="absolute right-3 top-3 rounded-full border border-white/10 bg-black/40 px-2 py-1 text-[11px] text-white/70 transition-colors hover:border-white/20 hover:text-white"
className={IMAGE_OVERLAY_DANGER_TEXT_CLASS}
>
删除
</button>
@ -7368,11 +7221,11 @@ export function AgentGenerationWorkspaceDarkV2({
}
/>
) : null}
<div className="absolute left-3 top-3 rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/78">
<div className={cn("absolute left-3 top-3", IMAGE_OVERLAY_BADGE_CLASS)}>
参考图 {index + 1}
</div>
{activeSkuReferenceCardId === card.id ? (
<div className="absolute left-3 top-12 rounded-full border border-emerald-300/35 bg-emerald-500/[0.14] px-2.5 py-1 text-[11px] text-emerald-100">
<div className={cn("absolute left-3 top-12", IMAGE_OVERLAY_ACTIVE_BADGE_CLASS)}>
当前联动
</div>
) : null}
@ -7482,7 +7335,7 @@ export function AgentGenerationWorkspaceDarkV2({
setFullscreenPreview(card.resultUrl!);
}}
/>
<div className={cn("absolute left-3 top-3 rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
<div className={cn("absolute left-3 top-3", IMAGE_OVERLAY_STATUS_BASE_CLASS, statusMeta.className)}>
{statusMeta.label}
</div>
<button
@ -7490,7 +7343,7 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={() =>
handleDeleteResultImage("sku_replacement", card.resultUrl!)
}
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
className={IMAGE_OVERLAY_DANGER_ICON_CLASS}
aria-label="删除结果图"
>
<Trash2 className="h-3.5 w-3.5" />
@ -7500,7 +7353,7 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={() => {
void handleDownloadOriginalImage(card.resultUrl!);
}}
className="absolute bottom-3 right-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute bottom-3 right-3", IMAGE_OVERLAY_ACTION_CLASS)}
>
下载
</button>
@ -7515,7 +7368,7 @@ export function AgentGenerationWorkspaceDarkV2({
title: `${selectedAgentPreset?.name || "SKU替换"} 结果图 ${card.index + 1}`,
})
}
className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={IMAGE_OVERLAY_ACTION_CLASS}
>
当前页改字
</button>
@ -7531,7 +7384,7 @@ export function AgentGenerationWorkspaceDarkV2({
});
setReplacementRerunPrompt("");
}}
className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20 disabled:cursor-not-allowed disabled:opacity-40"
className={cn(IMAGE_OVERLAY_ACTION_CLASS, "disabled:cursor-not-allowed disabled:opacity-45")}
>
重新生图
</button>
@ -7648,7 +7501,7 @@ export function AgentGenerationWorkspaceDarkV2({
setSelectedResultUrl(card.url!);
setSceneResizeEditorTargetUrl(card.url!);
}}
className="absolute left-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-emerald-400/28 bg-black/60 text-emerald-100 transition-colors hover:border-emerald-300/45 hover:bg-emerald-500/[0.16]"
className={cn("absolute left-3 top-3 flex h-9 w-9 items-center justify-center", IMAGE_OVERLAY_ACTIVE_BADGE_CLASS, "px-0 py-0")}
aria-label="调整尺寸"
>
<Maximize2 className="h-4 w-4" />
@ -7664,14 +7517,14 @@ export function AgentGenerationWorkspaceDarkV2({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
});
}}
className="absolute left-14 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute left-14 top-3 flex items-center gap-1", IMAGE_OVERLAY_ACTION_CLASS)}
aria-label="套用 PSD 模板"
>
<ImagePlus className="h-3.5 w-3.5" />
<span>套PSD</span>
</button>
) : null}
<div className="pointer-events-none absolute inset-x-3 bottom-3 rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-[11px] text-white/78 backdrop-blur">
<div className="pointer-events-none absolute inset-x-3 bottom-3 rounded-2xl border border-white/25 bg-zinc-950/82 px-3 py-2 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md">
双击查看大图
</div>
</>
@ -7687,7 +7540,7 @@ export function AgentGenerationWorkspaceDarkV2({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
});
}}
className="absolute left-3 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute left-3 top-3 flex items-center gap-1", IMAGE_OVERLAY_ACTION_CLASS)}
aria-label="套用 PSD 模板"
>
<ImagePlus className="h-3.5 w-3.5" />
@ -7700,7 +7553,7 @@ export function AgentGenerationWorkspaceDarkV2({
event.stopPropagation();
handleDeleteResultImage(activeResultModeType, card.url!);
}}
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
className={IMAGE_OVERLAY_DANGER_ICON_CLASS}
aria-label="删除结果图"
>
<Trash2 className="h-3.5 w-3.5" />
@ -7713,7 +7566,7 @@ export function AgentGenerationWorkspaceDarkV2({
event.stopPropagation();
void handleDownloadOriginalImage(card.url!);
}}
className="absolute bottom-3 right-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute bottom-3 right-3", IMAGE_OVERLAY_ACTION_CLASS)}
>
下载
</button>
@ -7726,7 +7579,7 @@ export function AgentGenerationWorkspaceDarkV2({
title: `AI详情 ${card.index + 1}`,
});
}}
className="absolute bottom-3 left-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20"
className={cn("absolute bottom-3 left-3", IMAGE_OVERLAY_ACTION_CLASS)}
>
送入画布改字
</button>

View File

@ -122,7 +122,7 @@ export function FloatingSidebarRail({
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
isHorizontal
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
: "w-[56px] flex-col overflow-x-hidden overflow-y-auto overscroll-contain rounded-[24px] px-2 py-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
: "w-[56px] max-w-[56px] min-w-0 flex-col overflow-x-hidden overflow-y-auto overscroll-contain rounded-[24px] px-2 py-2 [overflow-x:clip] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
className
)}
>
@ -153,7 +153,7 @@ export function FloatingSidebarAction({
<TooltipTrigger asChild>
<button
className={cn(
"flex h-10 w-10 items-center justify-center rounded-xl text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
active &&
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
)}

View File

@ -1502,7 +1502,7 @@ export function LovartHome({
const SelectedThemeIcon = selectedThemeOption.icon;
return (
<div className="min-h-svh bg-[#f4f4f4] text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
<div className="min-h-svh overflow-x-hidden bg-[#f4f4f4] text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
{routeTransitionLabel ? (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-[#050607] text-white/70">
<div className="flex flex-col items-center gap-4 rounded-[32px] border border-white/10 bg-white/[0.04] px-8 py-7 shadow-[0_24px_90px_rgba(0,0,0,0.42)] backdrop-blur-xl">

View File

@ -82,7 +82,7 @@ export function LovartSidebar({
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
const sidebarButtonClass = (active = false) =>
[
"group flex h-10 w-10 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
"group flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
active
? "bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
: "",

View File

@ -5,7 +5,6 @@ import { Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
interface SearchableSelectProps {
@ -102,7 +101,12 @@ export function SearchableSelect({
"border-white/12 bg-white/[0.04] text-white placeholder:text-white/28 focus-visible:ring-emerald-400/30 focus-visible:ring-offset-0"
)}
/>
<ScrollArea className="max-h-64">
<div
className={cn(
"max-h-64 overflow-y-auto overscroll-contain pr-1",
isDark && "agent-dark-scrollbar"
)}
>
<div className="space-y-1">
{filteredOptions.length > 0 ? (
filteredOptions.map((item) => {
@ -142,7 +146,7 @@ export function SearchableSelect({
</div>
)}
</div>
</ScrollArea>
</div>
</div>
</PopoverContent>
</Popover>

View File

@ -0,0 +1,201 @@
# Git 更新整理与用户通知(2026-04-29 至 2026-05-29)
生成日期:2026-05-29
统计分支:`HEAD` 所在分支
统计范围:`2026-04-29 00:00:00` 至 `2026-05-29 23:59:59`
提交总数:`311`
## 一句话结论
最近 30 天的更新,重点不是零散的小修小补,而是围绕 6 条主线持续打磨:`画布局部编辑与裁剪`、`Style Pro 风格化工作台`、`3D / 世界场景`、`PSD / 工作流预览 / 详情替换`、`上传兼容性与生成效率`、`整体稳定性与故障恢复`。
如果这份内容是发给普通用户,建议优先写用户能直接感知到的变化,不要把 `CI`、`依赖审计`、`日志治理`、`存储配置`、`发布校验` 这类研发内部工作直接写进通知。
## 这 30 天用户能感知到的更新
### 1. 画布局部编辑和裁剪能力明显增强
这一轮最明确的新能力,是无限画布里的图片局部编辑已经基本成型。
用户能感知到的变化:
- 可以直接在画布图片上框选局部区域,对某一块内容单独修改,而不是整张图重做。
- 支持在同一张图里添加多个局部编辑区域,更适合一次处理多个位置。
- 每个局部区域可以附带参考图,帮助 AI 更准确理解要改什么。
- 画布内新增裁剪面板,局部改图前的准备动作更顺。
- 中文输入、输入法、键盘输入、焦点丢失、拖动画布后标注错位等问题做了连续修复。
- 生成结果会尽量避免把框选标记、辅助标注带进最终图片。
对应提交线索:
- `feat: add in-canvas image crop panels`
- `feat: add canvas local image edit`
- `feat: add local edit to infinite canvas`
- `feat: add reference images to local edit regions`
- 2026-05-19 至 2026-05-23 多次修复局部编辑输入、对齐、中文输入和输出污染问题
### 2. Style Pro 从单点功能升级成完整工作台
`Style Pro` 这 30 天不是简单优化,而是补齐了更完整的创作链路。
用户能感知到的变化:
- 新增独立的 `Style Pro` 画布工作台,更适合围绕一张图连续做风格化创作。
- 增加设置页、预设项、历史记录、价格信息和更紧凑的输入布局,操作路径更完整。
- 新增 `真实感模式`,更适合往商业摄影、写实风格方向处理。
- 生成排队状态开始可见,等待时间更透明。
- 结果大图预览、模型可见性、计费说明和扣费逻辑更清晰。
- 图片尺寸和积分成本联动,生成前更容易理解不同尺寸的消耗差异。
对应提交线索:
- `feat: add style pro canvas workspace`
- `feat: add style pro settings tab`
- `feat: seed style pro index presets`
- `feat: persist style pro history and model pricing`
- `feat: add style pro realistic mode`
- `feat: surface image queue status across generators`
- `feat: add image size credit multipliers`
### 3. 3D / 世界场景体验更稳,画质和视角更合理
3D 和世界场景相关更新非常密集,说明这块经历了一轮集中打磨。
用户能感知到的变化:
- 3D / 世界查看器加载更快,卡住、一直转圈、不出结果的问题被重点处理。
- 默认视角、相机朝向、地平面对齐更合理,进入后更容易直接看对位置。
- 高质量渲染更稳定,黑边、材质丢失、贴图异常等问题做了修复。
- 放置热点时允许继续做 3D 视角浏览,交互更自然。
- 新增世界快照二次精修能力,可以在已有世界结果基础上继续优化画面。
对应提交线索:
- `perf: upgrade world viewer to spark 2`
- `perf: speed up world viewer loading`
- `feat: refine world snapshots with image generation`
- `fix: prevent world 3d viewer loading forever`
- `fix: recover stalled 3d world tasks`
- `fix: improve high quality world rendering`
- `fix: align world splat initial orientation`
### 4. PSD、工作流预览和详情图替换更可靠了
这部分虽然提交名偏内部,但对日常创作影响很直接。
用户能感知到的变化:
- 详情图替换支持更细粒度的处理方式,例如只替换产品主体,不重做整张图。
- 商品详情图开始支持引用更多参考信息,适合批量处理更复杂的素材。
- 工作流卡片即使没有预览图,也会有兜底展示,不容易出现空白卡片。
- 工作流保存对预览图的依赖降低,减少“配置好了但保存不了”的情况。
- 详情拼接、切线拖拽、长图预览和下载速度都做了优化。
- PSD 模板编辑、文字层渲染、图层命中、导出底图和静态层保留都更稳定。
对应提交线索:
- `feat: connect product detail image references`
- `feat: support batch product id reference fill`
- `feat: support product-only detail replacement`
- `feat: improve psd template layer editing`
- `fix: allow workflow saves without previews`
- `fix: avoid blank workflow preview cards`
- `fix: improve stitch cut line dragging`
- `fix: preserve psd render base in exports`
### 5. 上传兼容性和图片生成效率都有一轮提升
这一块对高频使用者感知会比较明显。
用户能感知到的变化:
- 图片上传更稳,失败兜底和重试能力更完整。
- 新增直传链路后,大图或批量上传更顺,减少中间转存失败。
- iPhone 常见的 `HEIC` 图片现在会自动转成 `JPEG` 再继续处理,兼容性更好。
- 图片生成并发和排队能力做了多轮优化,高峰期的处理效率更高。
- 批量图片处理、场景融合和工作流相关图片流转也更稳定。
对应提交线索:
- `feat: add direct multipart image uploads`
- `feat: convert heic uploads to jpeg`
- `feat: improve image generation concurrency`
- `feat: add image batch dispatch pacing`
- `feat: unify site-wide image concurrency`
- `feat: raise per-model image queue concurrency`
- `fix: stabilize canvas uploads and image load`
### 6. 底层故障恢复和整体稳定性补强了一轮
这部分不适合在用户通知里写得太技术化,但可以概括成“更稳、更少中断”。
用户能感知到的变化:
- 智能体生成进度反馈更明确,减少“系统没反应”的感觉。
- 首页、工作台和项目加载路径做了提速和死锁修复,进入使用状态更快。
- 图像生成底层增加了更多故障恢复和自动切换能力,模型临时不可用时更容易自动兜底。
- 一些失败场景的提示更清楚,用户更容易判断是在排队、重试还是生成失败。
对应提交线索:
- `feat: improve agent progress feedback`
- `feat: add ai model health failover`
- `feat: add ai model health recovery pool`
- `feat: add AI model health ban exclusion`
- `fix: unlock new agent project workspace`
- `fix: prevent agent workspace loading deadlock`
- `perf: speed up home and canvas entry`
## 建议你对外重点写的 6 件事
如果通知篇幅有限,最值得写清楚的是下面 6 件事:
1. `画布局部编辑升级`:支持多区域编辑、参考图辅助、中文输入更稳。
2. `Style Pro 升级`:新增真实感模式、历史记录、排队状态和更清晰的积分说明。
3. `画布裁剪更顺手`:新增画布内裁剪面板,改图前处理更方便。
4. `3D / 世界场景优化`:加载更快、更稳,视角和画质明显改善。
5. `PSD / 详情替换 / 工作流预览改进`:预览更完整,导出更稳定,空白和错位问题减少。
6. `上传兼容性提升`:支持 `HEIC` 自动转 `JPEG`,上传和批量处理成功率更高。
## 可直接发送给用户的通知文案
### 版本 A:适合群通知 / 公告栏
大家好,过去 30 天我们对产品做了一轮比较集中的升级,重点优化了画布编辑、风格化生成、3D 场景、PSD/详情图处理和上传体验。
这次更新里,画布局部编辑已经更完整了。现在可以在一张图里框选多个区域分别修改,也支持给局部区域添加参考图,帮助 AI 更准确理解你的修改目标。与此同时,中文输入、输入法、焦点丢失、拖动画布后标注错位等问题也做了集中修复,局部改图会顺手很多。我们也新增了画布内裁剪面板,让改图前的准备动作更方便。
`Style Pro` 也完成了一轮明显升级,新增了独立工作台、历史记录、真实感模式、排队状态提示和更清晰的积分说明,更适合围绕同一张图连续做风格化创作。3D / 世界场景这边则重点解决了加载慢、视角不准、画面卡住和材质异常等问题,整体打开速度、画质表现和交互流畅度都有提升。
另外,PSD 模板编辑、详情图替换、工作流预览和导出链路也更稳定了,空白预览、文字变形、底图丢失、切线拖拽不顺等问题都做了优化。上传方面,现在已支持 `HEIC` 图片自动转 `JPEG`,批量上传和图片生成高峰期的处理效率也有提升。
如果你最近遇到过局部编辑不好输入、3D 场景卡住、工作流预览空白、PSD 导出异常或图片上传失败之类的问题,这一轮更新会有比较明显的改善。欢迎继续反馈,我们会继续把高频使用链路打磨得更稳定、更好用。
### 版本 B:适合私域通知 / 版本更新说明
大家好,最近 30 天我们完成了一轮较大的功能更新,重点围绕创作效率、生成稳定性和上传兼容性做了持续优化。和大家使用最相关的变化如下:
- `画布局部编辑升级`:支持在一张图片中框选多个区域分别编辑,也支持给局部区域添加参考图。中文输入、输入法、焦点、拖动画布后的标注对齐,以及编辑痕迹误入结果图等问题都做了集中修复。
- `新增画布内裁剪面板`:裁剪、框选和后续局部编辑之间的操作衔接更顺了。
- `Style Pro 升级`:新增独立工作台、真实感模式、历史记录、排队状态提示和更清晰的积分说明,更适合连续做风格化创作。
- `3D / 世界场景优化`:加载更快、更稳,默认视角更合理,画质表现更好,并支持在已有世界结果基础上继续做精修。
- `PSD / 详情图 / 工作流优化`:详情图替换支持更细的处理方式,工作流预览缺失时会有兜底展示,PSD 导出时的文字变形、底图丢失和图层异常问题也做了修复。
- `上传兼容性提升`:现在支持 `HEIC` 图片自动转换为 `JPEG`,上传稳定性、批量处理能力和失败兜底也进一步增强。
这次更新还补强了一批底层稳定性能力,包括更清晰的生成进度反馈、图片生成链路的自动故障切换、首页和工作台的加载稳定性提升等。整体目标很明确,就是让大家在连续创作时更顺手、更少被中断。
感谢大家持续反馈问题,我们会继续把高频使用链路打磨得更稳定、更好用。
## 不建议直接写给普通用户的内容
下面这些提交很多,但更适合内部周报,不建议直接塞进对外通知:
- GitHub Actions、发布校验、依赖审计、质量门禁
- 日志治理、日志脱敏、敏感接口保护
- 七牛 Kodo 存储配置
- 仓库文档、模板、支持政策、安全政策
## 备注
- 这份整理基于本地 Git 历史,不包含未提交内容。
- 本时间窗内提交很多,以上内容已经按“用户最能感知”的口径做了筛选和归类。

View File

@ -0,0 +1,186 @@
# Git 更新整理与用户通知(2026-05-14 至 2026-05-29)
生成日期:2026-05-29
统计分支:`HEAD` 所在分支
统计范围:`2026-05-14 00:00:00` 至 `2026-05-29`
提交总数:`242`
## 一句话结论
最近 15 天的更新重点不在“加了很多零散按钮”,而是在 6 条主线能力上持续打磨:`画布局部编辑`、`Style Pro 风格化`、`3D / 世界场景`、`工作流预览与 PSD/详情替换`、`上传兼容性`、`整体稳定性与安全性`。
如果是发给普通用户,建议重点写用户能直接感知到的变化,不要把 `CI`、`依赖审计`、`日志治理`、`存储配置` 这类研发项直接写进通知。
## 这 15 天用户能感知到的更新
### 1. 画布局部编辑正式成型
这一轮最明显的功能升级,是无限画布里的“局部编辑”能力基本可用了。
用户能感知到的变化:
- 可以直接在画布图片上框选局部区域后,再针对某一块内容单独改图。
- 支持一个图片里添加多个编辑区域,适合一次处理多个位置。
- 每个编辑区域可以附带参考图,帮助 AI 更准确理解要改成什么样。
- 局部编辑输入框、焦点、中文输入法、键盘输入、拖动画布后的对齐问题都做了集中修复。
- 生成结果会尽量避开把标注框、辅助线、编辑痕迹带进最终图片。
对应提交线索:
- `feat: add canvas local image edit`
- `feat: add local edit to infinite canvas`
- `feat: add reference images to local edit regions`
- 之后连续多次修复输入、对齐、焦点、中文输入和输出污染问题
### 2. Style Pro 从基础能力升级到完整工作台
`Style Pro` 在这段时间里不只是修修补补,而是从单点能力升级成了更完整的风格化工作区。
用户能感知到的变化:
- 新增独立的 `Style Pro` 画布工作台,更适合围绕一张图连续做风格化创作。
- 增加预设项、历史记录、价格信息和结果回画布能力,操作路径更完整。
- 新增 `真实感模式`,更适合把素材往商业摄影、写实成片方向处理。
- 支持显示排队状态,等待时间更透明。
- 结果大图预览、模型显示、计费说明、积分扣费逻辑都更清晰。
- 图片尺寸和积分成本开始联动,用户在生成前更容易理解不同尺寸的消耗差异。
对应提交线索:
- `feat: add style pro canvas workspace`
- `feat: persist style pro history and model pricing`
- `feat: add style pro realistic mode`
- `feat: surface image queue status across generators`
- `feat: add image size credit multipliers`
### 3. 3D / 世界场景体验明显更稳了
3D 和世界场景相关提交非常密集,说明这块做了连续修复和质量提升。
用户能感知到的变化:
- 3D / 世界查看器加载更快,卡住、一直转圈、不出结果的情况被重点处理。
- 世界场景的默认视角、相机朝向、地平面对齐更合理,进入后不容易“看歪”。
- 世界场景清晰度和画质做了提升,高质量渲染效果更稳定。
- 放置热点时允许继续做 3D 视角浏览,交互更顺。
- 新增世界快照的二次精修能力,可以在已有世界结果基础上继续优化画面。
对应提交线索:
- `perf: speed up world viewer loading`
- `feat: refine world snapshots with image generation`
- `fix: recover stalled 3d world tasks`
- `fix: prevent world 3d viewer loading forever`
- `fix: improve high quality world rendering`
- `fix: align world splat initial orientation`
### 4. 工作流预览、详情替换和 PSD 结果更可靠
这部分更新很多,虽然名字偏内部,但用户实际会直接受益。
用户能感知到的变化:
- 详情图替换支持更精细的处理方式,例如只替换产品主体,不重做整张图。
- 详情拼接/切线拖拽、预览显示、长图预览格式都有优化。
- 工作流卡片即使没有预览图,也会有更合理的兜底展示,不容易出现空白卡片。
- 工作流保存对预览图的依赖降低,减少“明明配置好了却保存不了”的情况。
- PSD 渲染导出更稳定,文字层变形、底图丢失、静态层缺失等问题被持续修复。
对应提交线索:
- `feat: support product-only detail replacement`
- `fix: improve stitch cut line dragging`
- `fix: render auto workflow options without preview images`
- `fix: allow workflow saves without previews`
- `fix: render psd text layers without distortion`
- `fix: preserve psd render base in exports`
### 5. 上传链路和图片兼容性提升很大
这条线虽然偏基础能力,但对日常使用影响很大。
用户能感知到的变化:
- 上传过程更稳,失败重试和兜底处理更完整。
- 新增直传能力后,大文件或图片上传的链路更顺,减少中间转存带来的失败概率。
- iPhone 常见的 `HEIC` 图片现在会自动转换成 `JPEG` 再继续处理,兼容性更好。
- 图片加载和上传后的显示稳定性也有持续修复。
对应提交线索:
- `feat: add direct multipart image uploads`
- `feat: convert heic uploads to jpeg`
- `fix: harden image upload reliability`
- `fix: stabilize canvas uploads and image load`
- `fix: harden image upload fallback handling`
### 6. 稳定性、反馈和安全性补了一轮底层能力
这部分不适合在用户通知里写太技术化,但可以概括成“整体更稳定了”。
用户能感知到的变化:
- 生成队列状态、智能体进度反馈更明确,减少“系统没反应”的感觉。
- 部分失败场景的提示更清楚,便于判断是排队、重试还是生成失败。
- 首页、工作台和项目加载路径做了提速和死锁修复。
- 安全、日志脱敏、敏感接口保护、发布校验也补强了一轮,整体更适合正式交付。
对应提交线索:
- `feat: improve agent progress feedback`
- `fix: classify chat send failures`
- `fix: unlock new agent project workspace`
- `fix: prevent agent workspace loading deadlock`
- `Harden exposed security-sensitive endpoints`
- `Harden public attack surface`
## 建议你对外重点写的 5 件事
如果通知篇幅有限,最值得写清楚的是下面这 5 件事:
1. `画布局部编辑上线`:支持多区域编辑、参考图辅助、中文输入更稳。
2. `Style Pro 升级`:新增真实感模式、历史记录、排队状态和更清晰的积分说明。
3. `3D / 世界场景优化`:加载更快、更稳,视角和画质明显改善。
4. `详情替换 / PSD / 工作流预览改进`:预览更完整,导出更稳定,空白和错位问题减少。
5. `上传兼容性提升`:支持 HEIC 自动转 JPEG,上传失败率降低。
## 可直接发送给用户的通知文案
### 版本 A:简洁版
大家好,最近两周我们对产品做了一轮比较集中的升级,重点优化了画布编辑、风格化生成、3D 场景和上传体验。
本次更新中,画布局部编辑能力已经更完整了,支持在一张图里框选多个区域分别修改,还可以给局部区域添加参考图,中文输入、焦点和拖动画布时的对齐问题也一起做了修复。`Style Pro` 也完成了一轮升级,新增了真实感模式、历史记录、排队状态提示和更清晰的积分说明,整体更适合连续做风格化创作。
同时,我们对 3D / 世界场景做了大量稳定性优化,加载速度、默认视角、画质表现和交互流畅度都有明显提升;工作流预览、详情替换和 PSD 导出链路也更可靠了,空白预览、错位、文字变形、底图丢失等问题都做了集中修复。上传方面,现在已支持 `HEIC` 图片自动转 `JPEG`,整体上传成功率和兼容性也更好。
如果你最近在使用过程中遇到过局部编辑输入不顺、3D 场景卡住、预览空白或图片上传失败之类的问题,这一轮更新会有比较明显的改善。欢迎继续反馈,我们会持续优化。
### 版本 B:详细版
大家好,最近 15 天我们完成了一轮较大的功能更新,重点围绕创作效率、生成稳定性和上传兼容性做了持续优化。和大家使用最相关的变化如下:
- `画布局部编辑升级`:现在可以在一张图片中框选多个区域分别编辑,并支持给局部区域添加参考图,帮助 AI 更准确理解修改目标。同时,中文输入、输入框焦点、拖动画布后的标注对齐、编辑痕迹误入结果图等问题都做了集中修复。
- `Style Pro 升级`:新增独立工作台、真实感模式、历史记录、排队状态提示和更清晰的积分说明,适合围绕同一张图做连续风格化创作。
- `3D / 世界场景优化`:加载更快、更稳,默认视角更合理,画质表现更好,热点放置时的浏览操作也更顺畅,并支持在已有世界结果基础上继续做精修。
- `工作流 / 详情替换 / PSD 优化`:详情图替换支持更细的处理方式,工作流预览缺失时会有兜底展示,PSD 导出时的文字变形、底图丢失、图层异常等问题也做了修复。
- `上传兼容性提升`:现在支持 `HEIC` 图片自动转换为 `JPEG`,上传过程的稳定性和失败兜底能力也进一步增强。
这次更新还补强了一批底层稳定性和安全能力,包括更清晰的生成进度反馈、失败提示优化、工作台加载稳定性提升等。整体目标很明确,就是让大家在连续创作时更顺手、更少被中断。
感谢大家持续反馈问题,我们会继续把高频使用链路打磨得更稳定、更好用。
## 不建议直接写给普通用户的内容
下面这些提交很多,但更适合内部周报,不建议直接塞进对外通知:
- GitHub Actions、发布校验、依赖审计、质量门禁
- 日志治理、脱敏、分支检查、品牌检查
- 七牛 Kodo 存储配置
- 第三方外部图片生成 API 接口能力
- 仓库文档、模板、支持政策、安全政策
## 备注
- 这份整理基于本地 Git 历史,不包含未提交内容。
- 本时间窗内提交非常多,以上内容已经按“用户最能感知”的口径做了筛选和归类。

View File

@ -0,0 +1,410 @@
# 风格化功能商业宣传片旁白教程(成交版)
> 适用页面:风格化功能页,非风格化 PRO。
> 线上入口:`https://ai.yydd.ai`
> 目标用户:非专业电商运营。
> 核心目标:前 8 秒同时打出核心卖点:0 图 0 文字、自动找图、AI 卖点优化、爆款复刻、场景替换保留排版、产品替换、风格一致、行业首创闭环,让用户立刻知道产品强在哪里。
## 黄金 8 秒开场
不用找图,不用写词,不用等设计。
0 图 0 文字,点选生成整套商业图。
SKU 自动找图,AI 自动优化卖点。
爆款 ID 自动拆详情,场景替换保留排版,产品替换套用结构。
按人群、场景和风格,锁定一套能卖货的视觉表达。
素材库、3D、PSD、详情模板打通,行业首创闭环交付。
风格化,电商运营的 AI 商业视觉生产线。
## 20 秒开场旁白
### 画面
快切:运营翻素材库、找 SKU 图、催设计、改 prompt、结果风格不统一。
随后切到风格化页面:不上传图、不输入字,只点选模式、品牌、场景、风格。
再切到:输入爆款详情 ID,系统自动拉取竞品详情。
最后展示:一组统一风格的主图 / SKU 图 / 详情图。
### 旁白
今天给大家演示风格化全新升级。
不用备图、不写词,0 图 0 文字,选模式、品牌、场景和风格,就能成套生成主图、SKU 图和详情图。
系统自动匹配 SKU / SPU 素材,基于人群优化卖点。
输入爆款 ID 解析竞品结构;场景替换保留版式,产品替换植入爆款结构。
素材库、3D、PSD、详情模板打通,生成结果可直接套 PSD 模板、接详情模板,形成行业首创 AI 商业视觉闭环。
## 正片旁白
### 1. 进入风格化工作台
**画面**:从首页进入风格化页面。
**旁白**:
进入风格化,不是从写 prompt 开始。
而是从业务场景开始。
你要主图、SKU 图、详情图,直接选对应模式。
系统会自动接好后面的素材、风格、生成和交付流程。
运营不用再从零想,不用再问设计怎么描述。
### 2. 0 图 0 文字点选生图
**画面**:不上传图片,不输入文字,只点选模式、品牌、场景、风格。
**旁白**:
先看最短路径。
不上传图。
不写提示词。
只点选模式、品牌、场景和风格。
系统自动调用我们沉淀好的生活场景化优质提示词。
什么人群、什么空间、什么光线、什么生活氛围、商品怎么出现,后台都已经组织好。
运营不需要懂摄影,不需要懂构图,也不需要懂 AI 提示词。
点完,直接生成。
### 3. 风格一致,不再一张一个样
**画面**:展示同一风格下生成的一组主图 / SKU 图 / 场景图。
**旁白**:
普通 AI 生图最大的问题,是不稳定。
今天一个风格,明天一个风格。
一组图放在一起,像不同店铺做出来的。
风格化工作流不一样。
它会把品牌、人群、场景、光线和风格规则固定下来。
后面不管生成主图、SKU 图还是详情图,都沿用同一套视觉规则。
这才是商业素材真正需要的:不是单张好看,而是一组统一。
### 4. AI 卖点优化
**画面**:展示目标人群、风格需求、商品卖点输入或选择;系统生成优化后的卖点文案并带入图片/详情。
**旁白**:
出图之外,真正影响转化的,是卖点怎么说。
同一个商品,卖给宝妈、白领、学生、礼品人群,表达方式完全不一样。
风格化工作流会结合目标人群、场景氛围和视觉风格,自动优化商品卖点。
不是生硬堆参数。
而是把功能点转成用户听得懂、愿意停留、愿意下单的购买理由。
比如主打品质感,就强化材质、细节和高级感。
主打生活场景,就强化使用状态、情绪价值和代入感。
主打促销转化,就强化利益点、稀缺感和决策理由。
图片负责吸引,卖点负责成交。
这一步,系统一起帮运营做好。
### 5. SKU / SPU 自动找图
**画面**:输入 SKU / SPU,系统拉取白底图、候选图;展示上传商品图自动抠图。
**旁白**:
真实商品落地,也不用到处翻图。
知道 SKU,就搜 SKU。
只有 SPU,就先选 SKU 再拉图。
系统自动把商品素材拉出来。
不用在聊天记录、网盘、素材库、商品后台里翻来翻去。
没有合适图,也可以直接上传商品图。
系统自动抠图,自动带入生图槽位。
找图、抠图、整理图,这些最耗时间的脏活,系统先帮你做掉。
### 6. 爆款详情复刻
**画面**:进入详情模式,输入市场爆款详情 ID,系统展示竞品详情图和整套素材。
**旁白**:
详情页,才是最能拉开差距的地方。
以前看到同行爆款,你要自己找链接、截图、爬图、裁长图、整理素材。
还要拆它为什么卖得好。
现在不用。
输入市场爆款详情 ID。
系统自动获取竞品整套详情。
几乎所有主流平台都支持。
爆款版式、图片顺序、卖点节奏、信息层级,系统帮你整理出来。
这不是复制图片。
这是复刻爆款的成交结构。
你选中爆款结构,把自己的商品放进去,就能快速生成自己的详情页。
### 7. 详情双模式:场景替换 / 产品替换
**画面**:展示详情模式中的“场景替换”和“产品替换”入口;切换模式后展示不同生成结果。
**旁白**:
详情生成,不是简单换一张图。
这里有两种核心模式。
第一种,场景替换。
它的关键,不是把背景换掉。
而是在保留原详情排版、文字结构、信息层级和视觉节奏的前提下,替换成新的生活场景。
目前行业里大多数 AI 模型,一做场景替换,排版容易乱,文字容易坏,商品位置也容易跑偏。
我们的优势,是场景可以换,版式能保持。
同一个商品,可以快速做出居家场景、户外场景、礼品场景、品质感场景,但整套详情依然稳定、统一、可交付。
第二种,产品替换。
系统保留爆款详情的版式、构图、信息层级和卖点节奏。
把竞品里的商品,替换成你自己的商品。
一个负责把场景做对。
一个负责把爆款结构用起来。
这才是真正适合运营的详情生产方式。
### 8. 拼合切割与批量替换
**画面**:展示详情图拼合切割、标记不处理、开始批量替换、结果逐张回显。
**旁白**:
长图不用自己切。
系统帮你拼合、切割、分屏。
不需要处理的片段,直接标记直出。
需要替换的部分,交给 AI 批量生成。
每一屏对应一张结果。
哪一张好,哪一张要重做,一眼看清。
这不是黑盒生成。
这是运营能掌控的详情生产流程。
### 9. 改字、重生、批量下载
**画面**:展示当前页改字、重新生图、下载、批量下载。
**旁白**:
生成完不是结束。
能交付,才算结束。
文字不合适,当前页直接改。
某一屏不满意,只重生这一屏。
整套确认后,一键批量下载。
不用反复丢给设计。
运营自己就能完成检查、修改、重跑、打包。
### 10. 素材库 / 3D / PSD / 详情模板闭环
**画面**:展示素材库、3D 模型、PSD 模板、详情模板;展示结果图套 PSD、接详情模板、导出。
**旁白**:
真正厉害的地方,不是只会生图。
我们已经打通素材库系统。
商品素材、3D 模型、PSD 模板、详情模板,全部连起来。
以前这些东西是分开的。
素材在素材库。
模型在模型库。
模板在设计那里。
详情又是另一套流程。
现在不是。
素材进来,可以直接生图。
有 3D 模型,可以接视觉生产。
生成结果,可以直接套 PSD 模板。
做详情,可以接详情模板和爆款结构。
从素材、生成、套版、详情到批量交付,一条链路跑到底。
这才不是普通 AI 工具。
这是行业首创的 AI 商业视觉生产闭环。
## 结尾旁白
### 画面
快速回放:0 图 0 文字生图、生活场景提示词、风格统一、AI 卖点优化、SKU/SPU 自动找图、输入爆款 ID、竞品详情获取、场景替换、产品替换、套 PSD、模板交付。
### 旁白
运营要的不是又一个复杂工具。
运营要的是更快出图,更少找图,更少返工。
风格化工作流,把提示词、卖点优化、素材库、竞品详情、3D 模型、PSD 模板、详情模板,全部打通。
0 图 0 文字,点点就能生图。
输入爆款详情 ID,就能复刻市场爆款结构。
场景替换,在保留详情排版的前提下,快速生成不同人群需要的生活场景。
产品替换,把自己的商品放进爆款详情结构。
同一套工作流,让主图、SKU 图、详情图保持统一。
AI 根据人群、场景和风格优化卖点,让图片不只是好看,而是更会卖。
从找图到出图,从爆款参考到详情落地,从素材库到模板交付,系统统统帮你搞定。
这就是行业首创的 AI 商业视觉生产闭环。
## 3 分钟快剪版
### 快剪开场
运营做图,最耗时间的不是创意。
是找图、写词、等设计、反复改。
普通 AI 生图,还容易一张一个风格。
风格化工作流,把专业提示词变成可点选流程。
0 图,0 文字。
不准备图片,不写提示词。
点选模式、品牌、场景和风格,系统自动调用生活场景化优质提示词,直接生成统一的商业视觉。
AI 还会根据目标人群和风格需求,自动优化商品卖点。
做详情更狠。
输入市场爆款详情 ID,系统自动获取竞品整套详情。
爆款版式、图片结构、卖点节奏,直接帮你整理好。
详情支持场景替换和产品替换。
场景替换,不是普通换背景,而是在保留详情排版、文字结构和信息层级的前提下,把画面替换成更适合目标人群的生活场景。
产品替换,把自己的商品放进爆款详情结构。
不用爬图,不用截图,不用裁长图,不用整理素材。
选中爆款结构,就能快速生成自己的详情页。
更关键的是,素材库、3D 模型、PSD 模板、详情模板已经全面打通。
不是生成完就结束。
而是从素材到模板,从详情到交付,一条链路跑到底。
这套完整闭环,属于行业首创。
### 快剪教程
进入风格化。
先选模式,再选品牌、场景和风格。
不上传图片,不输入文字,直接点击生成。
这就是 0 图 0 文字生图。
卖点不用硬写,AI 会结合人群和风格自动优化。
要真实商品,就按 SKU / SPU 自动找图;没有合适图,就上传自动抠图。
要做详情,就输入爆款详情 ID,系统自动拉取竞品整套详情。
选择场景替换,在保留原详情排版的前提下,生成更适合人群和风格的生活场景。
选择产品替换,把自己的商品放进爆款详情结构。
结果出来后,可以改字、重生、套 PSD、接详情模板、批量下载。
从一张图都没有,到一套可交付素材,运营自己就能完成。
## 录屏节奏建议
| 时间段 | 画面重点 | 旁白重点 |
| --- | --- | --- |
| 0:00-0:20 | 痛点快切 + 风格化入口 | 找图、写词、风格飘、等设计、反复改 |
| 0:20-1:00 | 0 图 0 文字点选生图 | 不上传、不写词,直接生成 |
| 1:00-1:35 | 风格卡片与提示词工作流 | 生活场景化提示词,锁定风格一致 |
| 1:35-2:05 | AI 卖点优化 | 基于人群、场景和风格优化购买理由 |
| 2:05-2:40 | SKU/SPU 自动找图 | 不翻素材库,系统自动拉图 |
| 2:40-3:40 | 爆款详情 ID 获取竞品详情 | 不爬图不裁图,复刻成交结构 |
| 3:40-4:30 | 详情双模式 | 场景替换保留排版换场景,产品替换套爆款结构 |
| 4:30-5:20 | 批量替换与结果管理 | 改字、重生、批量下载 |
| 5:20-6:20 | 素材库 / 3D / PSD / 详情模板闭环 | 行业首创闭环,从素材到模板交付 |
| 6:20-6:50 | 总结收尾 | 点点生图,爆款复刻,闭环交付 |
## 视频简介文案
风格化功能是一套专为电商运营设计的 AI 商业视觉生产线,解决找图耗时、不会写 prompt、设计排期紧、SKU/SPU 对不上、生成风格不统一、反复改图效率低等问题。0 图 0 文字也能生图:不准备图片,不手写提示词,只需点选模式、品牌、场景和风格,系统自动调用生活场景化优质提示词,并锁定同一套视觉规则,让主图、SKU 图、详情图更有代入感,也更容易保持风格一致。系统还支持 AI 卖点优化,能基于目标人群、使用场景和风格需求,把商品功能点优化成更容易打动用户的购买理由。做详情时,只需输入市场爆款详情 ID,系统自动获取竞品整套详情,提炼爆款版式、图片结构和卖点节奏,无需自己爬图、截图、裁图、整理图片。详情支持场景替换模式和产品替换模式:场景替换不是普通换背景,而是在保留详情排版、文字结构和信息层级的前提下,把画面替换成更适合目标人群的生活场景,解决大多数 AI 模型一换场景就排版错乱、文字损坏、商品跑偏的问题;产品替换则把自己的商品放进爆款详情结构。同时打通素材库、3D 模型、PSD 模板和详情模板,形成从素材、生成、套版、详情到批量交付的行业首创完整闭环,适合非专业运营快速制作高质感商品主图、SKU 图、详情图和投放素材。