fix agent workflow freshness and goods info prompts
This commit is contained in:
parent
519239bcd3
commit
8df3173cb2
@ -3116,12 +3116,17 @@ function buildAgentSlotPrompt(params: {
|
||||
function buildAgentSceneWorkflowPrompt(params: {
|
||||
workflowPrompt?: string | null;
|
||||
userSupplementPrompt?: string | null;
|
||||
imageRolePrompt?: string | null;
|
||||
}) {
|
||||
const lines: string[] = [];
|
||||
const workflowPrompt = String(params.workflowPrompt || "").trim();
|
||||
if (workflowPrompt) {
|
||||
lines.push(workflowPrompt);
|
||||
}
|
||||
const imageRolePrompt = String(params.imageRolePrompt || "").trim();
|
||||
if (imageRolePrompt) {
|
||||
lines.push(imageRolePrompt);
|
||||
}
|
||||
const supplement = String(params.userSupplementPrompt || "").trim();
|
||||
if (supplement) {
|
||||
lines.push(`【补充要求】${supplement}`);
|
||||
@ -3129,6 +3134,56 @@ function buildAgentSceneWorkflowPrompt(params: {
|
||||
return lines.join("\n\n").trim();
|
||||
}
|
||||
|
||||
function buildAgentSceneImageRolePrompt(params: {
|
||||
uploadedImageCount: number;
|
||||
productImageCount: number;
|
||||
includeModelImage: boolean;
|
||||
}) {
|
||||
if (params.uploadedImageCount <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const lines = ["【输入图片角色说明】"];
|
||||
const resolvedProductCount = Math.max(
|
||||
1,
|
||||
Math.min(params.productImageCount, params.uploadedImageCount)
|
||||
);
|
||||
const hasModelImage =
|
||||
params.includeModelImage && params.uploadedImageCount > resolvedProductCount;
|
||||
|
||||
for (let index = 0; index < params.uploadedImageCount; index += 1) {
|
||||
if (index < resolvedProductCount) {
|
||||
lines.push(
|
||||
`图片${index + 1}:产品白底参考图,用于锁定产品的外观、比例、材质、颜色、结构和细节。`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasModelImage && index === resolvedProductCount) {
|
||||
lines.push(
|
||||
`图片${index + 1}:固定模特图。若画面需要人物,必须复用这张图中的同一模特(脸型、发型、体型、肤色、穿搭风格保持一致),不得替换成其他人物。`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`图片${index + 1}:补充参考图,只用于补充产品角度、细节或辅助信息,不可替代主产品外观。`
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`必须以图片1${resolvedProductCount > 1 ? `到图片${resolvedProductCount}` : ""}中的产品信息为最高优先级,严格保持同一产品。`
|
||||
);
|
||||
lines.push("不要把多张输入图拼贴成多个主体,也不要把所有输入图元素强行同时融入结果。");
|
||||
lines.push(
|
||||
hasModelImage
|
||||
? `如果当前场景有人物展示需求,只允许使用图片${resolvedProductCount + 1}里的模特;如果不需要人物,则不要强行加入人物。`
|
||||
: "当前没有固定模特图,不要凭空生成强人物主体。"
|
||||
);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
type AgentReplacementModePayload = NonNullable<PostRequestBody["agentModePayload"]>;
|
||||
|
||||
type ReplacementReferenceDecision = {
|
||||
@ -6554,6 +6609,12 @@ export async function POST(request: Request) {
|
||||
const isSkuReplacementPreset =
|
||||
selectedAgentPreset.type === "image" &&
|
||||
presetModeConfig.modeType === "sku_replacement";
|
||||
const sceneProductSlotCount = isSceneGenerationPreset
|
||||
? Math.max(1, presetModeConfig.scene.productImageSlotCount || 1)
|
||||
: 0;
|
||||
const sceneModelSlotEnabled = isSceneGenerationPreset
|
||||
? presetModeConfig.scene.includeModelSlot
|
||||
: false;
|
||||
const replacementProductSlotCount = isDetailReplacementPreset
|
||||
? presetModeConfig.detail.productImageSlotCount
|
||||
: isSkuReplacementPreset
|
||||
@ -7221,9 +7282,15 @@ export async function POST(request: Request) {
|
||||
[...sharedUploadedAssetIds, ...(sceneSlot?.referenceAssetIds || [])].filter(Boolean),
|
||||
),
|
||||
);
|
||||
const sceneImageRolePrompt = buildAgentSceneImageRolePrompt({
|
||||
uploadedImageCount: sharedUploadedImageUrls.length,
|
||||
productImageCount: sceneProductSlotCount,
|
||||
includeModelImage: sceneModelSlotEnabled,
|
||||
});
|
||||
const scenePrompt =
|
||||
buildAgentSceneWorkflowPrompt({
|
||||
workflowPrompt: hiddenPromptText || null,
|
||||
imageRolePrompt: sceneImageRolePrompt,
|
||||
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
|
||||
}) ||
|
||||
buildAgentSlotPrompt({
|
||||
|
||||
@ -327,6 +327,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
>({});
|
||||
const [selectedBrandModel, setSelectedBrandModel] =
|
||||
useState<AgentWorkflowSelectedBrandModel | null>(null);
|
||||
const workflowInputsRef = useRef(workflowInputs);
|
||||
const workflowMaterialsRef = useRef(workflowMaterials);
|
||||
const workflowSelectionsRef = useRef(workflowSelections);
|
||||
const selectedBrandModelRef = useRef(selectedBrandModel);
|
||||
const [selectedAgentPresetId, setSelectedAgentPresetId] = useState<string | null>(null);
|
||||
const [currentAgentStage, setCurrentAgentStage] = useState<"workflow" | "mode">("workflow");
|
||||
const [agentModeState, setAgentModeState] = useState<AgentModeWorkspaceState>(
|
||||
@ -1707,6 +1711,49 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
workflowMaterials,
|
||||
workflowSelections,
|
||||
]);
|
||||
useEffect(() => {
|
||||
workflowInputsRef.current = workflowInputs;
|
||||
}, [workflowInputs]);
|
||||
useEffect(() => {
|
||||
workflowMaterialsRef.current = workflowMaterials;
|
||||
}, [workflowMaterials]);
|
||||
useEffect(() => {
|
||||
workflowSelectionsRef.current = workflowSelections;
|
||||
}, [workflowSelections]);
|
||||
useEffect(() => {
|
||||
selectedBrandModelRef.current = selectedBrandModel;
|
||||
}, [selectedBrandModel]);
|
||||
const handleWorkflowInputsChange = useCallback((next: AgentWorkflowInputs) => {
|
||||
workflowInputsRef.current = next;
|
||||
setWorkflowInputs(next);
|
||||
}, []);
|
||||
const handleWorkflowMaterialsChange = useCallback((next: AgentWorkflowMaterialSelections) => {
|
||||
workflowMaterialsRef.current = next;
|
||||
setWorkflowMaterials(next);
|
||||
}, []);
|
||||
const handleWorkflowSelectionsChange = useCallback(
|
||||
(next: Partial<Record<AgentWorkflowStepKey, string>>) => {
|
||||
workflowSelectionsRef.current = next;
|
||||
setWorkflowSelections(next);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleSelectedBrandModelChange = useCallback(
|
||||
(next: AgentWorkflowSelectedBrandModel | null) => {
|
||||
selectedBrandModelRef.current = next;
|
||||
setSelectedBrandModel(next);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const getLatestWorkflowContext = useCallback(() => {
|
||||
return buildAgentWorkflowComposerContext({
|
||||
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
||||
inputs: workflowInputsRef.current,
|
||||
materials: workflowMaterialsRef.current,
|
||||
selectedBrandModel: selectedBrandModelRef.current,
|
||||
selections: workflowSelectionsRef.current,
|
||||
});
|
||||
}, [resolvedWorkflowUiConfig]);
|
||||
const { data: selectedAgentPresetResponse } = useSWR<{
|
||||
success?: boolean;
|
||||
data?: AgentPreset;
|
||||
@ -1896,6 +1943,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
selectedAgentPresetId={isAgentWorkspaceEnabled ? selectedAgentPresetId : null}
|
||||
selectedBrandModel={selectedBrandModel}
|
||||
workflowContext={workflowContext}
|
||||
getLatestWorkflowContext={getLatestWorkflowContext}
|
||||
agentModeState={agentModeState}
|
||||
onAgentModeStateChange={setAgentModeState}
|
||||
workspaceState={agentWorkspaceState}
|
||||
@ -1920,7 +1968,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
<AgentWorkflowBrandStorePanel
|
||||
config={resolvedWorkflowUiConfig}
|
||||
inputs={workflowInputs}
|
||||
onInputsChange={setWorkflowInputs}
|
||||
onInputsChange={handleWorkflowInputsChange}
|
||||
theme="dark"
|
||||
/>
|
||||
}
|
||||
@ -1929,11 +1977,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
embedded
|
||||
config={resolvedWorkflowUiConfig}
|
||||
inputs={workflowInputs}
|
||||
onInputsChange={setWorkflowInputs}
|
||||
onInputsChange={handleWorkflowInputsChange}
|
||||
materials={workflowMaterials}
|
||||
onMaterialsChange={setWorkflowMaterials}
|
||||
onMaterialsChange={handleWorkflowMaterialsChange}
|
||||
selections={workflowSelections}
|
||||
onSelectionsChange={setWorkflowSelections}
|
||||
onSelectionsChange={handleWorkflowSelectionsChange}
|
||||
showBrandStoreSection={false}
|
||||
/>
|
||||
}
|
||||
@ -1950,16 +1998,16 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
<AgentWorkflowBrandModelPicker
|
||||
brand={workflowInputs.brand}
|
||||
selectedBrandModel={selectedBrandModel}
|
||||
onSelectBrandModel={setSelectedBrandModel}
|
||||
onSelectBrandModel={handleSelectedBrandModelChange}
|
||||
theme="dark"
|
||||
/>
|
||||
}
|
||||
materialSelectionPanel={
|
||||
<AgentWorkflowMaterialSelectionPanel
|
||||
inputs={workflowInputs}
|
||||
onInputsChange={setWorkflowInputs}
|
||||
onInputsChange={handleWorkflowInputsChange}
|
||||
materials={workflowMaterials}
|
||||
onMaterialsChange={setWorkflowMaterials}
|
||||
onMaterialsChange={handleWorkflowMaterialsChange}
|
||||
productSelectionLimit={materialProductSelectionLimit}
|
||||
disabled={!selectedAgentPresetId}
|
||||
theme="dark"
|
||||
|
||||
@ -104,6 +104,7 @@ interface AgentGenerationWorkspaceDarkV2Props {
|
||||
selectedAgentPresetId?: string | null;
|
||||
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
|
||||
workflowContext?: AgentWorkflowComposerContext | null;
|
||||
getLatestWorkflowContext?: () => AgentWorkflowComposerContext | null;
|
||||
onBack?: () => void;
|
||||
presetPanel?: ReactNode;
|
||||
brandStorePanel?: ReactNode;
|
||||
@ -850,6 +851,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
selectedAgentPresetId,
|
||||
selectedBrandModel = null,
|
||||
workflowContext = null,
|
||||
getLatestWorkflowContext,
|
||||
onBack,
|
||||
presetPanel,
|
||||
brandStorePanel,
|
||||
@ -2457,9 +2459,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
if (!attachment) return;
|
||||
parts.push({ type: "file", mediaType: attachment.contentType || "image/jpeg", name: attachment.name || "参考图", url: attachment.url, assetId: attachment.assetId });
|
||||
});
|
||||
const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext;
|
||||
let textContent = input.trim();
|
||||
if (!textContent && selectedAgentPreset?.type === "text" && workflowContext?.summaryText) {
|
||||
textContent = `请基于当前工作流设定生成方案:${workflowContext.summaryText}`;
|
||||
if (!textContent && selectedAgentPreset?.type === "text" && resolvedWorkflowContext?.summaryText) {
|
||||
textContent = `请基于当前工作流设定生成方案:${resolvedWorkflowContext.summaryText}`;
|
||||
}
|
||||
if (!textContent && selectedAgentPreset?.type === "text" && selectedAgentPreset) {
|
||||
textContent = `请按「${selectedAgentPreset.name}」输出内容`;
|
||||
@ -2471,8 +2474,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
toast.error("请至少补充要求或上传图片");
|
||||
return;
|
||||
}
|
||||
const nextHiddenPrompt = workflowContext?.promptText
|
||||
? `【工作流设定】\n${workflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。`
|
||||
const nextHiddenPrompt = resolvedWorkflowContext?.promptText
|
||||
? `【工作流设定】\n${resolvedWorkflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。`
|
||||
: null;
|
||||
hiddenPromptRef.current = nextHiddenPrompt;
|
||||
generationRequestSnapshotRef.current = {
|
||||
@ -2522,8 +2525,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
setPendingGeneration,
|
||||
skuReplacementReferenceCards.length,
|
||||
userMessageCount,
|
||||
workflowContext?.promptText,
|
||||
workflowContext?.summaryText,
|
||||
getLatestWorkflowContext,
|
||||
workflowContext,
|
||||
]);
|
||||
const handleStopGeneration = useCallback(() => {
|
||||
detailGenerationCancelRef.current = true;
|
||||
|
||||
@ -110,6 +110,12 @@ export function AgentWorkflowMaterialPicker({
|
||||
value: result?.goodsInfo?.boardType || null,
|
||||
},
|
||||
].filter((item) => Boolean(item.value));
|
||||
const goodsInfoMissingHint =
|
||||
result?.queryType === "sku" &&
|
||||
Boolean(String(result?.matchedSku || result?.query || "").trim()) &&
|
||||
goodsInfoItems.length === 0
|
||||
? `当前 SKU「${result?.matchedSku || result?.query}」暂未从商品信息接口匹配到商品名称、尺寸或材质信息。`
|
||||
: "";
|
||||
const remainingSelectableCount =
|
||||
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
|
||||
const totalImageCount = result?.images?.length || 0;
|
||||
@ -125,6 +131,16 @@ export function AgentWorkflowMaterialPicker({
|
||||
const title = result?.goodsInfo?.productName || result?.matchedSku || result?.query || "当前SKU";
|
||||
return `${title} · 系统已自动选中 ${selectedCount} 张图`;
|
||||
}, [result?.goodsInfo?.productName, result?.matchedSku, result?.query, selectedCount]);
|
||||
const collapsedGoodsInfoSummary = useMemo(
|
||||
() =>
|
||||
[
|
||||
result?.goodsInfo?.sizeInfo ? `尺寸:${result.goodsInfo.sizeInfo}` : "",
|
||||
result?.goodsInfo?.materialInfo ? `材质:${result.goodsInfo.materialInfo}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
[result?.goodsInfo?.materialInfo, result?.goodsInfo?.sizeInfo]
|
||||
);
|
||||
const visibleImages = useMemo(() => {
|
||||
if (!result?.images?.length) {
|
||||
return [];
|
||||
@ -323,6 +339,11 @@ export function AgentWorkflowMaterialPicker({
|
||||
<div className="mt-1 text-xs text-emerald-100/72">
|
||||
默认先替你收起结果区,需要检查商品信息、已选图片或手动微调时再展开查看。
|
||||
</div>
|
||||
{collapsedGoodsInfoSummary ? (
|
||||
<div className="mt-2 text-xs text-emerald-100/82">
|
||||
{collapsedGoodsInfoSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@ -518,6 +539,19 @@ export function AgentWorkflowMaterialPicker({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{goodsInfoMissingHint ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[20px] border px-3 py-2 text-xs",
|
||||
isDark
|
||||
? "border-amber-500/20 bg-amber-500/10 text-amber-100"
|
||||
: "border-amber-200 bg-amber-50 text-amber-800"
|
||||
)}
|
||||
>
|
||||
{goodsInfoMissingHint}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{result.skuOptions?.length ? (
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
{result.skuOptions.map((option) => (
|
||||
|
||||
@ -23,6 +23,66 @@ export interface AgentWorkflowInputsValue {
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
function buildGoodsInfoPromptLine(params: {
|
||||
label: string;
|
||||
result?: AgentWorkflowMaterialSelections["primary"]["result"] | null;
|
||||
}) {
|
||||
const goodsInfo = params.result?.goodsInfo;
|
||||
if (!goodsInfo) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parts = [
|
||||
goodsInfo.productName ? `商品名称:${goodsInfo.productName}` : "",
|
||||
goodsInfo.sizeInfo ? `尺寸信息:${goodsInfo.sizeInfo}` : "",
|
||||
goodsInfo.materialInfo ? `材质信息:${goodsInfo.materialInfo}` : "",
|
||||
goodsInfo.designer ? `设计师:${goodsInfo.designer}` : "",
|
||||
goodsInfo.buyer ? `采购负责人:${goodsInfo.buyer}` : "",
|
||||
goodsInfo.boardType ? `板材类型:${goodsInfo.boardType}` : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${params.label}:${parts.join(";")}`;
|
||||
}
|
||||
|
||||
function buildGoodsInfoSizeConstraintLine(params: {
|
||||
label: string;
|
||||
result?: AgentWorkflowMaterialSelections["primary"]["result"] | null;
|
||||
}) {
|
||||
const goodsInfo = params.result?.goodsInfo;
|
||||
const sizeInfo = String(goodsInfo?.sizeInfo || "").trim();
|
||||
if (!sizeInfo) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${params.label}尺寸比例约束:尺寸信息为 ${sizeInfo}。生成场景时必须依据该尺寸信息判断产品在空间中的真实体量、长宽高关系和与周边家具/人物/通道的比例,不得随意放大、缩小或改变产品结构比例。`;
|
||||
}
|
||||
|
||||
function buildGoodsInfoSummaryLine(params: {
|
||||
label: string;
|
||||
result?: AgentWorkflowMaterialSelections["primary"]["result"] | null;
|
||||
}) {
|
||||
const goodsInfo = params.result?.goodsInfo;
|
||||
if (!goodsInfo) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parts = [
|
||||
goodsInfo.productName || "",
|
||||
goodsInfo.sizeInfo || "",
|
||||
goodsInfo.materialInfo || "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${params.label}:${parts.join(" / ")}`;
|
||||
}
|
||||
|
||||
function normalizeWorkflowText(value: string) {
|
||||
return String(value || "")
|
||||
.replace(/\s+/g, " ")
|
||||
@ -126,6 +186,20 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
if (params.inputs.priceBand.trim()) {
|
||||
summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`);
|
||||
}
|
||||
const primaryGoodsInfoSummary = buildGoodsInfoSummaryLine({
|
||||
label: "主SKU商品信息",
|
||||
result: params.materials?.primary?.result || null,
|
||||
});
|
||||
if (primaryGoodsInfoSummary) {
|
||||
summaryParts.push(primaryGoodsInfoSummary);
|
||||
}
|
||||
const secondaryGoodsInfoSummary = buildGoodsInfoSummaryLine({
|
||||
label: "搭配SKU商品信息",
|
||||
result: params.materials?.secondary?.result || null,
|
||||
});
|
||||
if (secondaryGoodsInfoSummary) {
|
||||
summaryParts.push(secondaryGoodsInfoSummary);
|
||||
}
|
||||
if (referenceImages.some((item) => item.source === "primary")) {
|
||||
summaryParts.push(
|
||||
`主SPU图:${referenceImages.filter((item) => item.source === "primary").length}张`
|
||||
@ -169,6 +243,34 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
if (params.inputs.priceBand.trim()) {
|
||||
promptLines.push(`价格带:${params.inputs.priceBand.trim()}`);
|
||||
}
|
||||
const primaryGoodsInfoPrompt = buildGoodsInfoPromptLine({
|
||||
label: "主SKU商品信息",
|
||||
result: params.materials?.primary?.result || null,
|
||||
});
|
||||
if (primaryGoodsInfoPrompt) {
|
||||
promptLines.push(primaryGoodsInfoPrompt);
|
||||
}
|
||||
const primaryGoodsInfoSizeConstraint = buildGoodsInfoSizeConstraintLine({
|
||||
label: "主SKU",
|
||||
result: params.materials?.primary?.result || null,
|
||||
});
|
||||
if (primaryGoodsInfoSizeConstraint) {
|
||||
promptLines.push(primaryGoodsInfoSizeConstraint);
|
||||
}
|
||||
const secondaryGoodsInfoPrompt = buildGoodsInfoPromptLine({
|
||||
label: "搭配SKU商品信息",
|
||||
result: params.materials?.secondary?.result || null,
|
||||
});
|
||||
if (secondaryGoodsInfoPrompt) {
|
||||
promptLines.push(secondaryGoodsInfoPrompt);
|
||||
}
|
||||
const secondaryGoodsInfoSizeConstraint = buildGoodsInfoSizeConstraintLine({
|
||||
label: "搭配SKU",
|
||||
result: params.materials?.secondary?.result || null,
|
||||
});
|
||||
if (secondaryGoodsInfoSizeConstraint) {
|
||||
promptLines.push(secondaryGoodsInfoSizeConstraint);
|
||||
}
|
||||
if (referenceImages.some((item) => item.source === "primary")) {
|
||||
const labels = referenceImages
|
||||
.filter((item) => item.source === "primary")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user