fix agent workflow freshness and goods info prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-26 00:08:26 +08:00
parent 519239bcd3
commit 8df3173cb2
5 changed files with 267 additions and 13 deletions

View File

@ -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({

View File

@ -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"

View File

@ -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;

View File

@ -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) => (

View File

@ -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")