From 1c4fe1a836ddd13b9caba1aeb46edc61eac50145 Mon Sep 17 00:00:00 2001 From: joey-home Date: Wed, 16 Sep 2026 22:03:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E8=A7=89=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/collector/src/api/client.ts | 10 ++--- server/api/products.py | 1 + server/schemas/suite.py | 2 + server/services/jobs.py | 3 +- server/services/prompts/common.py | 10 ++--- studio/src/pages/trial/TrialImageGrids.tsx | 5 +++ studio/src/pages/trial/TrialSuitePanel.tsx | 49 ++++++++++++---------- studio/src/pages/trial/TrialVideoMaker.tsx | 2 +- studio/src/services/suite.ts | 12 +++--- 9 files changed, 54 insertions(+), 40 deletions(-) diff --git a/extensions/collector/src/api/client.ts b/extensions/collector/src/api/client.ts index 7529379..4d403d2 100644 --- a/extensions/collector/src/api/client.ts +++ b/extensions/collector/src/api/client.ts @@ -38,27 +38,27 @@ export const STYLE_SET_OPTIONS = [ { value: 1, label: '北欧极简', - prompt: '北欧极简风:浅灰或米白背景,柔和漫射光,低饱和色调,画面留白充足,构图克制干净', + prompt: 'Nordic editorial e-commerce style for contextual and marketing images only: warm ivory or light greige base, soft window daylight with a subtle warm practical-light accent, low-saturation natural palette, pale wood and tactile linen details, spacious orderly composition, and at most one or two restrained lifestyle props. Keep the product exact and prominent. When text is requested, reserve a clean top text-safe zone with a clear headline-first hierarchy. Never override a task-mandated pure-white background, no-text, or no-props instruction', }, { value: 2, label: '清新明亮', - prompt: '清新明亮风:明亮的白色到浅蓝渐变背景,高调光线,色彩明快通透,整体轻盈干净', + prompt: 'Fresh, bright, family-friendly e-commerce style for contextual and marketing images only: crisp white with pale sky-blue, mint, or other accents sampled from the product palette; high-key daylight, luminous but natural color, soft clean shadows, airy mobile-first composition, and a cheerful polished retail mood. When text is requested, use a generous top text-safe zone, one bold headline plus one short supporting line or pill, and small simple accent icons only when they clarify a real feature. Never override a task-mandated pure-white background, no-text, or no-props instruction', }, { value: 3, label: '高级感深色', - prompt: '高级质感风:深灰或炭黑背景,戏剧性侧光打光,突出商品材质与光泽,沉稳高级', + prompt: 'Premium dark editorial e-commerce style for contextual and marketing images only: deep charcoal, graphite, or near-black setting; sculpted side and rim light; controlled contrast; realistic material texture and sheen; one restrained tonal prop at most; refined, quiet, high-end mood. Let a single product-derived color accent carry the composition. When text is requested, keep a large high-contrast headline in a deliberate text-safe area and a minimal icon or specification row. Never override a task-mandated pure-white background, no-text, or no-props instruction', }, { value: 4, label: '暖调生活', - prompt: '温暖生活风:暖米色背景,暖色灯光氛围,温馨的家居质感,亲和力强', + prompt: 'Warm, cozy lifestyle e-commerce style for contextual and marketing images only: a believable cream, ivory, or warm-beige home setting; soft golden lamp light balanced with gentle window daylight; tactile bedding, knit, or natural wood texture; comforting and approachable atmosphere. Stage one simple usage story with the product as the unquestioned hero; use only category-relevant props and, if appropriate, a natural hand or person interacting with it. For family or children products, a softly styled nursery or bedroom is suitable. When text is requested, use a top headline zone, bold rounded sans-serif lettering, a short secondary pill or line, and small friendly feature icons. Never override a task-mandated pure-white background, no-text, or no-props instruction', }, { value: 5, label: '纯净棚拍', - prompt: '标准电商棚拍:纯色浅背景,均匀的正面柔光,无杂物干扰,商品居中突出', + prompt: 'Clean conversion-focused e-commerce studio style: seamless pure white or very light neutral background only where the task permits, even frontal softbox lighting with a subtle natural contact shadow, faithful color, crisp edge separation, and product filling the frame at a readable scale. No decorative props, lifestyle scenery, artificial reflections, or visual clutter. If the task explicitly requires copy or specifications, use a precise grid, a clear headline-first hierarchy, and sparse line icons; otherwise show no text. Never alter the product or contradict a task-specific background instruction', }, ] as const; diff --git a/server/api/products.py b/server/api/products.py index d42fe3f..ca949d8 100644 --- a/server/api/products.py +++ b/server/api/products.py @@ -179,6 +179,7 @@ async def list_assets( "width": a.width, "height": a.height, "error": a.error, + "created_at": a.created_at, } for a in rows ] diff --git a/server/schemas/suite.py b/server/schemas/suite.py index 5e53574..90c59bc 100644 --- a/server/schemas/suite.py +++ b/server/schemas/suite.py @@ -148,11 +148,13 @@ class SuiteCreateResponse(BaseModel): class SuiteImageOut(BaseModel): item_id: str + asset_id: str | None = None type_id: str name: str url: str status: str error: str | None = None + completed_at: datetime | None = None class SuiteOut(BaseModel): diff --git a/server/services/jobs.py b/server/services/jobs.py index b4b06a7..a72276a 100644 --- a/server/services/jobs.py +++ b/server/services/jobs.py @@ -489,11 +489,12 @@ def suite_view(job: BackgroundJob, items: list[BackgroundJobItem]) -> dict[str, "created_at": job.created_at, "started_at": job.started_at, "completed_at": job.completed_at, "images": [ { + "asset_id": str(item.generated_asset_id) if item.generated_asset_id else None, "type_id": (item.payload or {}).get("kind", "custom"), "item_id": str(item.id), "name": (item.payload or {}).get("title", "AI 生图"), "url": item.result_url or "", "status": item.status, - "error": item.error, + "error": item.error, "completed_at": item.completed_at, } for item in items ], diff --git a/server/services/prompts/common.py b/server/services/prompts/common.py index 0bd3bfe..f5ab503 100644 --- a/server/services/prompts/common.py +++ b/server/services/prompts/common.py @@ -12,27 +12,27 @@ import re STYLE_SETS: dict[int, dict] = { 1: { "name": "北欧极简", - "tone": "Nordic minimalism: light gray or warm off-white background, soft diffused light, low-saturation palette, generous negative space, restrained and clean composition", + "tone": "Nordic editorial e-commerce style for contextual and marketing images only: warm ivory or light greige base, soft window daylight with a subtle warm practical-light accent, low-saturation natural palette, pale wood and tactile linen details, spacious orderly composition, and at most one or two restrained lifestyle props. Keep the product exact and prominent. When text is requested, reserve a clean top text-safe zone with a clear headline-first hierarchy. Never override a task-mandated pure-white background, no-text, or no-props instruction", "bg": "", }, 2: { "name": "清新明亮", - "tone": "Fresh and bright: white-to-pale-blue gradient background, high-key lighting, clear luminous colors, airy and uncluttered composition", + "tone": "Fresh, bright, family-friendly e-commerce style for contextual and marketing images only: crisp white with pale sky-blue, mint, or other accents sampled from the product palette; high-key daylight, luminous but natural color, soft clean shadows, airy mobile-first composition, and a cheerful polished retail mood. When text is requested, use a generous top text-safe zone, one bold headline plus one short supporting line or pill, and small simple accent icons only when they clarify a real feature. Never override a task-mandated pure-white background, no-text, or no-props instruction", "bg": "", }, 3: { "name": "高级感深色", - "tone": "Premium dark aesthetic: deep gray or charcoal background, dramatic side lighting, emphasize material texture and sheen, refined and restrained mood", + "tone": "Premium dark editorial e-commerce style for contextual and marketing images only: deep charcoal, graphite, or near-black setting; sculpted side and rim light; controlled contrast; realistic material texture and sheen; one restrained tonal prop at most; refined, quiet, high-end mood. Let a single product-derived color accent carry the composition. When text is requested, keep a large high-contrast headline in a deliberate text-safe area and a minimal icon or specification row. Never override a task-mandated pure-white background, no-text, or no-props instruction", "bg": "", }, 4: { "name": "暖调生活", - "tone": "Warm lifestyle: warm beige background, cozy warm lighting, inviting home-living texture, approachable and natural atmosphere", + "tone": "Warm, cozy lifestyle e-commerce style for contextual and marketing images only: a believable cream, ivory, or warm-beige home setting; soft golden lamp light balanced with gentle window daylight; tactile bedding, knit, or natural wood texture; comforting and approachable atmosphere. Stage one simple usage story with the product as the unquestioned hero; use only category-relevant props and, if appropriate, a natural hand or person interacting with it. For family or children products, a softly styled nursery or bedroom is suitable. When text is requested, use a top headline zone, bold rounded sans-serif lettering, a short secondary pill or line, and small friendly feature icons. Never override a task-mandated pure-white background, no-text, or no-props instruction", "bg": "", }, 5: { "name": "纯净棚拍", - "tone": "Clean e-commerce studio: plain light background, even frontal softbox lighting, no distracting props, product centered and prominent", + "tone": "Clean conversion-focused e-commerce studio style: seamless pure white or very light neutral background only where the task permits, even frontal softbox lighting with a subtle natural contact shadow, faithful color, crisp edge separation, and product filling the frame at a readable scale. No decorative props, lifestyle scenery, artificial reflections, or visual clutter. If the task explicitly requires copy or specifications, use a precise grid, a clear headline-first hierarchy, and sparse line icons; otherwise show no text. Never alter the product or contradict a task-specific background instruction", "bg": "", }, } diff --git a/studio/src/pages/trial/TrialImageGrids.tsx b/studio/src/pages/trial/TrialImageGrids.tsx index b179086..d1fd8a0 100644 --- a/studio/src/pages/trial/TrialImageGrids.tsx +++ b/studio/src/pages/trial/TrialImageGrids.tsx @@ -164,6 +164,11 @@ interface GeneratedImageGridProps { export function GeneratedImageGrid({ images, selectedKeys, downloadingKeys, preview, onToggle, onDelete, onDownload, onEdit }: GeneratedImageGridProps) { const [page, setPage] = useState(1); const { current, visible } = useVisiblePage(images, page); + // 新完成的图片经过时间倒序会进入首位;无论用户此前停在哪一页,都直接展示最新结果。 + const newestKey = images[0]?.key; + useEffect(() => { + setPage(1); + }, [newestKey]); useEffect(() => { if (current !== page) setPage(current); }, [current, page]); diff --git a/studio/src/pages/trial/TrialSuitePanel.tsx b/studio/src/pages/trial/TrialSuitePanel.tsx index 09904c9..eaf4c34 100644 --- a/studio/src/pages/trial/TrialSuitePanel.tsx +++ b/studio/src/pages/trial/TrialSuitePanel.tsx @@ -215,15 +215,18 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr const suiteImages = (suite?.images ?? []) .filter((img) => img.status === 'done') .map((img, index) => { - const asset = generatedAssets.find((a) => assetUrl(a) === img.url); + const asset = generatedAssets.find((a) => a.id === img.asset_id) + || generatedAssets.find((a) => assetUrl(a) === img.url); return { - key: `suite-${suite?.id ?? 'current'}-${index}`, + // 素材已回写时与历史记录共用同一个 key,避免同一张图显示两次。 + key: asset ? `asset-${asset.id}` : `suite-${suite?.id ?? 'current'}-${img.item_id || index}`, url: img.url, name: img.name, - groupName: '套图结果', + groupName: '生成结果', variantName: null as string | null, assetId: asset?.id, - source: 'suite' as const, + source: asset ? 'history' as const : 'suite' as const, + createdAt: asset?.created_at || img.completed_at || suite?.completed_at || suite?.created_at || undefined, }; }); const historyImages = generatedAssets.map((asset) => ({ @@ -236,10 +239,21 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr source: 'history' as const, createdAt: asset.created_at, })); - return [...suiteImages, ...historyImages].filter((img) => !hiddenResultKeys.has(img.key)); + const unique = new Map(); + // 已落库的素材带有最终 created_at,后写入可覆盖同 key 的临时 suite 结果。 + [...suiteImages, ...historyImages].forEach((image) => unique.set(image.key, image)); + return [...unique.values()] + .filter((img) => !hiddenResultKeys.has(img.key)) + .sort((left, right) => { + const timestamp = (value?: string) => { + const parsed = value ? Date.parse(value) : Number.NaN; + return Number.isFinite(parsed) ? parsed : 0; + }; + const rightTime = timestamp(right.createdAt); + const leftTime = timestamp(left.createdAt); + return rightTime - leftTime || right.key.localeCompare(left.key); + }); }, [generatedAssets, hiddenResultKeys, suite]); - const suiteResultImages = resultImages.filter((img) => img.source === 'suite'); - const historyResultImages = resultImages.filter((img) => img.source === 'history'); const otherActiveSuiteTasks = activeSuiteTasks.filter((task) => task.id !== suite?.id); const selectedResultCount = resultImages.filter((img) => selectedResultKeys.has(img.key)).length; const selectedWatermarkItems = resultImages.filter((img) => selectedResultKeys.has(img.key) && img.assetId); @@ -1380,37 +1394,26 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr ) : null} - image.assetId && void handleDeleteAsset(image.assetId, [image.key, `asset-${image.assetId}`])} - onDownload={(image) => void handleDownloadResultImage(image)} - onEdit={(image) => setGenModal({ url: assetById.get(image.assetId || '')?.original_url || image.url, name: image.name, afterAssetId: image.assetId })} - /> image.status === 'failed')} /> {suite.error && } )} - {/* 单张 AI 生图结果(采集图片区「AI 生图」生成,回写 generated 组) */} - {historyResultImages.length > 0 && ( + {resultImages.length > 0 && (
- 生图记录 + 全部生成结果 - (一键生成与单张生图的落库历史,刷新后仍保留 · {historyResultImages.length} 张) + (按完成时间倒序 · 最新结果在第一页首位 · {resultImages.length} 张)
image.assetId && void handleDeleteAsset(image.assetId, [image.key])} + onDelete={(image) => image.assetId && void handleDeleteAsset(image.assetId, [image.key, `asset-${image.assetId}`])} onDownload={(image) => void handleDownloadResultImage(image)} onEdit={(image) => setGenModal({ url: assetById.get(image.assetId || '')?.original_url || image.url, name: image.name, afterAssetId: image.assetId })} /> diff --git a/studio/src/pages/trial/TrialVideoMaker.tsx b/studio/src/pages/trial/TrialVideoMaker.tsx index f0f6ef5..b15d5a6 100644 --- a/studio/src/pages/trial/TrialVideoMaker.tsx +++ b/studio/src/pages/trial/TrialVideoMaker.tsx @@ -98,7 +98,7 @@ export default function TrialVideoMaker({ productId, assets }: { productId: stri {jobs.filter((job) => job.status !== 'cancelled').length === 0 ? 生成的轮播视频会保存在这里,可随时下载。 :
{jobs.filter((job) => job.status !== 'cancelled').slice(0, 8).map((job) => void cancel(id)} onDelete={(id) => void remove(id)} onRetry={(id) => void retry(id)} onDownload={(id) => void download(id)} />)}
} setOpen(false)} onOk={() => void submit()} okText={`合成视频(${selected.length}/5)`} okButtonProps={{ disabled: selected.length < 2, loading: submitting }} width={900} title="选择轮播图片"> - {candidates.length < 2 ? :
{candidates.map((asset) => { const index = selected.indexOf(asset.id); return
toggle(asset.id)} style={{ cursor: 'pointer', border: `2px solid ${index >= 0 ? '#1677ff' : 'transparent'}`, borderRadius: 7, padding: 2, position: 'relative' }}>{index >= 0 && {index + 1}}
{asset.group_key === 'generated' ? 'AI 图 · ' : ''}{nameOf(asset)}
; })}
} + {candidates.length < 2 ? :
{candidates.map((asset) => { const index = selected.indexOf(asset.id); return
toggle(asset.id)} style={{ cursor: 'pointer', border: `2px solid ${index >= 0 ? '#1677ff' : 'transparent'}`, borderRadius: 7, padding: 2, position: 'relative' }}>{index >= 0 && {index + 1}}
{asset.group_key === 'generated' ? 'AI 图 · ' : ''}{nameOf(asset)}
; })}
} {selected.length > 0 && 播放顺序即选中序号;取消后重新选择可调整顺序。}
; diff --git a/studio/src/services/suite.ts b/studio/src/services/suite.ts index db7ec66..ca91f9c 100644 --- a/studio/src/services/suite.ts +++ b/studio/src/services/suite.ts @@ -54,11 +54,11 @@ export interface ProductDimensions { /** 视觉风格(默认提示词可在页面上改写,随生成请求覆盖后端模板) */ export const STYLE_SET_OPTIONS = [ - { value: 1, label: '北欧极简', prompt: 'Nordic minimalism: light gray or warm off-white background, soft diffused light, low-saturation palette, generous negative space, restrained and clean composition' }, - { value: 2, label: '清新明亮', prompt: 'Fresh and bright: white-to-pale-blue gradient background, high-key lighting, clear luminous colors, airy and uncluttered composition' }, - { value: 3, label: '高级感深色', prompt: 'Premium dark aesthetic: deep gray or charcoal background, dramatic side lighting, emphasize material texture and sheen, refined and restrained mood' }, - { value: 4, label: '暖调生活', prompt: 'Warm lifestyle: warm beige background, cozy warm lighting, inviting home-living texture, approachable and natural atmosphere' }, - { value: 5, label: '纯净棚拍', prompt: 'Clean e-commerce studio: plain light background, even frontal softbox lighting, no distracting props, product centered and prominent' }, + { value: 1, label: '北欧极简', prompt: 'Nordic editorial e-commerce style for contextual and marketing images only: warm ivory or light greige base, soft window daylight with a subtle warm practical-light accent, low-saturation natural palette, pale wood and tactile linen details, spacious orderly composition, and at most one or two restrained lifestyle props. Keep the product exact and prominent. When text is requested, reserve a clean top text-safe zone with a clear headline-first hierarchy. Never override a task-mandated pure-white background, no-text, or no-props instruction' }, + { value: 2, label: '清新明亮', prompt: 'Fresh, bright, family-friendly e-commerce style for contextual and marketing images only: crisp white with pale sky-blue, mint, or other accents sampled from the product palette; high-key daylight, luminous but natural color, soft clean shadows, airy mobile-first composition, and a cheerful polished retail mood. When text is requested, use a generous top text-safe zone, one bold headline plus one short supporting line or pill, and small simple accent icons only when they clarify a real feature. Never override a task-mandated pure-white background, no-text, or no-props instruction' }, + { value: 3, label: '高级感深色', prompt: 'Premium dark editorial e-commerce style for contextual and marketing images only: deep charcoal, graphite, or near-black setting; sculpted side and rim light; controlled contrast; realistic material texture and sheen; one restrained tonal prop at most; refined, quiet, high-end mood. Let a single product-derived color accent carry the composition. When text is requested, keep a large high-contrast headline in a deliberate text-safe area and a minimal icon or specification row. Never override a task-mandated pure-white background, no-text, or no-props instruction' }, + { value: 4, label: '暖调生活', prompt: 'Warm, cozy lifestyle e-commerce style for contextual and marketing images only: a believable cream, ivory, or warm-beige home setting; soft golden lamp light balanced with gentle window daylight; tactile bedding, knit, or natural wood texture; comforting and approachable atmosphere. Stage one simple usage story with the product as the unquestioned hero; use only category-relevant props and, if appropriate, a natural hand or person interacting with it. For family or children products, a softly styled nursery or bedroom is suitable. When text is requested, use a top headline zone, bold rounded sans-serif lettering, a short secondary pill or line, and small friendly feature icons. Never override a task-mandated pure-white background, no-text, or no-props instruction' }, + { value: 5, label: '纯净棚拍', prompt: 'Clean conversion-focused e-commerce studio style: seamless pure white or very light neutral background only where the task permits, even frontal softbox lighting with a subtle natural contact shadow, faithful color, crisp edge separation, and product filling the frame at a readable scale. No decorative props, lifestyle scenery, artificial reflections, or visual clutter. If the task explicitly requires copy or specifications, use a precise grid, a clear headline-first hierarchy, and sparse line icons; otherwise show no text. Never alter the product or contradict a task-specific background instruction' }, ] as const; /** 试算页固定目标平台 Ozon:俄文文案 · 3:4 图片 */ @@ -130,11 +130,13 @@ export interface SuiteGeneratePayload { export interface SuiteImageInfo { item_id: string; + asset_id?: string | null; type_id: string; name: string; url: string; status: string; error?: string | null; + completed_at?: string | null; } export interface SuiteInfo {