417 lines
13 KiB
TypeScript
417 lines
13 KiB
TypeScript
import {
|
||
defaultAgentWorkflowUiConfig,
|
||
flattenVisibleAgentWorkflowSteps,
|
||
getAgentWorkflowSelectedOptionPath,
|
||
getEnabledAgentWorkflowStepTree,
|
||
isAgentWorkflowStepSelectionComplete,
|
||
} from "@/lib/agents/workflow-ui-config";
|
||
import type {
|
||
AgentWorkflowComposerContext,
|
||
AgentWorkflowComposerSelection,
|
||
AgentWorkflowMaterialSelections,
|
||
AgentWorkflowReferenceImage,
|
||
AgentWorkflowSelectedBrandModel,
|
||
AgentWorkflowStepKey,
|
||
AgentWorkflowUiConfig,
|
||
} from "@/types/agent";
|
||
|
||
export interface AgentWorkflowInputsValue {
|
||
primarySku: string;
|
||
secondarySku: string;
|
||
brand: string;
|
||
store: string;
|
||
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, " ")
|
||
.trim();
|
||
}
|
||
|
||
function buildGroupedSelectionLines(params: {
|
||
selections: AgentWorkflowComposerSelection[];
|
||
valueKey: "optionName" | "promptFragment";
|
||
separator: string;
|
||
}) {
|
||
const grouped = new Map<
|
||
string,
|
||
{
|
||
stepTitle: string;
|
||
values: string[];
|
||
valueSet: Set<string>;
|
||
}
|
||
>();
|
||
|
||
params.selections.forEach((item) => {
|
||
const rawValue =
|
||
params.valueKey === "promptFragment"
|
||
? item.promptFragment || item.optionName
|
||
: item.optionName;
|
||
const normalizedValue = normalizeWorkflowText(rawValue || "");
|
||
if (!normalizedValue) {
|
||
return;
|
||
}
|
||
|
||
const groupKey = item.stepKey;
|
||
const currentGroup = grouped.get(groupKey) || {
|
||
stepTitle: item.stepTitle,
|
||
values: [],
|
||
valueSet: new Set<string>(),
|
||
};
|
||
|
||
const dedupeKey = normalizedValue.toLowerCase();
|
||
if (!currentGroup.valueSet.has(dedupeKey)) {
|
||
currentGroup.valueSet.add(dedupeKey);
|
||
currentGroup.values.push(normalizedValue);
|
||
}
|
||
|
||
grouped.set(groupKey, currentGroup);
|
||
});
|
||
|
||
return Array.from(grouped.values())
|
||
.filter((group) => group.values.length > 0)
|
||
.map((group) => `${group.stepTitle}:${group.values.join(params.separator)}`);
|
||
}
|
||
|
||
export function buildAgentWorkflowComposerContext(params: {
|
||
config?: AgentWorkflowUiConfig | null;
|
||
inputs: AgentWorkflowInputsValue;
|
||
materials?: AgentWorkflowMaterialSelections | null;
|
||
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
|
||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||
}): AgentWorkflowComposerContext | null {
|
||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||
const steps = flattenVisibleAgentWorkflowSteps(
|
||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||
params.selections
|
||
)
|
||
.map(({ step }) => step)
|
||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||
const selections = steps
|
||
.map((step) => {
|
||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||
step,
|
||
selectionValue: params.selections[step.key],
|
||
});
|
||
if (selectedOptions.length === 0) return [];
|
||
return selectedOptions.map((option) => ({
|
||
stepKey: step.key,
|
||
stepTitle: step.title,
|
||
optionKey: option.key,
|
||
optionName: option.buttonName,
|
||
promptFragment: option.promptFragment || option.buttonName,
|
||
}));
|
||
})
|
||
.flat() as AgentWorkflowComposerContext["selections"];
|
||
const referenceImages = getSelectedWorkflowReferenceImages({
|
||
inputs: params.inputs,
|
||
materials: params.materials,
|
||
selectedBrandModel: params.selectedBrandModel,
|
||
});
|
||
|
||
const summaryParts: string[] = [];
|
||
if (params.inputs.primarySku.trim()) {
|
||
summaryParts.push(`主SKU:${params.inputs.primarySku.trim()}`);
|
||
}
|
||
if (params.inputs.secondarySku.trim()) {
|
||
summaryParts.push(`搭配SKU:${params.inputs.secondarySku.trim()}`);
|
||
}
|
||
if (params.inputs.brand.trim()) {
|
||
summaryParts.push(`品牌:${params.inputs.brand.trim()}`);
|
||
}
|
||
if (params.inputs.store.trim()) {
|
||
summaryParts.push(`店铺:${params.inputs.store.trim()}`);
|
||
}
|
||
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}张`
|
||
);
|
||
}
|
||
if (referenceImages.some((item) => item.source === "secondary")) {
|
||
summaryParts.push(
|
||
`搭配SKU图:${referenceImages.filter((item) => item.source === "secondary").length}张`
|
||
);
|
||
}
|
||
if (referenceImages.some((item) => item.source === "brandModel")) {
|
||
summaryParts.push(
|
||
`品牌模特图:${referenceImages.filter((item) => item.source === "brandModel").length}张`
|
||
);
|
||
}
|
||
summaryParts.push(
|
||
...buildGroupedSelectionLines({
|
||
selections,
|
||
valueKey: "optionName",
|
||
separator: " / ",
|
||
})
|
||
);
|
||
|
||
if (summaryParts.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const promptLines: string[] = [];
|
||
if (params.inputs.primarySku.trim()) {
|
||
promptLines.push(`主SKU:${params.inputs.primarySku.trim()}`);
|
||
}
|
||
if (params.inputs.secondarySku.trim()) {
|
||
promptLines.push(`搭配SKU:${params.inputs.secondarySku.trim()}`);
|
||
}
|
||
if (params.inputs.brand.trim()) {
|
||
promptLines.push(`品牌:${params.inputs.brand.trim()}`);
|
||
}
|
||
if (params.inputs.store.trim()) {
|
||
promptLines.push(`店铺:${params.inputs.store.trim()}`);
|
||
}
|
||
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")
|
||
.map((item) => item.label)
|
||
.filter(Boolean)
|
||
.slice(0, 8);
|
||
promptLines.push(
|
||
`主SPU参考图:已选${referenceImages.filter((item) => item.source === "primary").length}张${
|
||
labels.length > 0 ? `(${labels.join("、")})` : ""
|
||
}`
|
||
);
|
||
}
|
||
if (referenceImages.some((item) => item.source === "secondary")) {
|
||
const labels = referenceImages
|
||
.filter((item) => item.source === "secondary")
|
||
.map((item) => item.label)
|
||
.filter(Boolean)
|
||
.slice(0, 8);
|
||
promptLines.push(
|
||
`搭配SKU参考图:已选${referenceImages.filter((item) => item.source === "secondary").length}张${
|
||
labels.length > 0 ? `(${labels.join("、")})` : ""
|
||
}`
|
||
);
|
||
}
|
||
if (referenceImages.some((item) => item.source === "brandModel")) {
|
||
const labels = referenceImages
|
||
.filter((item) => item.source === "brandModel")
|
||
.map((item) => item.label)
|
||
.filter(Boolean)
|
||
.slice(0, 8);
|
||
promptLines.push(
|
||
`品牌模特图:已选${referenceImages.filter((item) => item.source === "brandModel").length}张${
|
||
labels.length > 0 ? `(${labels.join("、")})` : ""
|
||
}`
|
||
);
|
||
}
|
||
promptLines.push(
|
||
...buildGroupedSelectionLines({
|
||
selections,
|
||
valueKey: "promptFragment",
|
||
separator: ";",
|
||
})
|
||
);
|
||
|
||
return {
|
||
primarySku: params.inputs.primarySku.trim() || undefined,
|
||
secondarySku: params.inputs.secondarySku.trim() || undefined,
|
||
brand: params.inputs.brand.trim() || undefined,
|
||
store: params.inputs.store.trim() || undefined,
|
||
priceBand: params.inputs.priceBand.trim() || undefined,
|
||
selections,
|
||
referenceImages,
|
||
summaryText: summaryParts.join(" / "),
|
||
promptText: promptLines.join("\n"),
|
||
};
|
||
}
|
||
|
||
export function getSelectedWorkflowReferenceImages(params: {
|
||
inputs: AgentWorkflowInputsValue;
|
||
materials?: AgentWorkflowMaterialSelections | null;
|
||
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
|
||
}): AgentWorkflowReferenceImage[] {
|
||
const materials = params.materials;
|
||
if (!materials) return [];
|
||
|
||
const collect = (
|
||
source: "primary" | "secondary",
|
||
sourceLabel: string,
|
||
fallbackSku: string
|
||
) => {
|
||
const state = materials[source];
|
||
const result = state?.result;
|
||
if (!result || !Array.isArray(result.images) || result.images.length === 0) {
|
||
return [] as AgentWorkflowReferenceImage[];
|
||
}
|
||
|
||
const selectedIds = new Set(
|
||
(state.selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)
|
||
);
|
||
if (selectedIds.size === 0) {
|
||
return [] as AgentWorkflowReferenceImage[];
|
||
}
|
||
|
||
return result.images
|
||
.filter((item) => selectedIds.has(item.id))
|
||
.map((item) => ({
|
||
id: item.id,
|
||
source,
|
||
sourceLabel,
|
||
sku: result.matchedSku || fallbackSku || undefined,
|
||
productModel: result.productModel || null,
|
||
label: item.label,
|
||
name: item.fileName || item.label,
|
||
url: item.imageUrl,
|
||
assetId: item.assetId || null,
|
||
mimeType: item.mimeType || null,
|
||
}));
|
||
};
|
||
|
||
return [
|
||
...collect("primary", "主SPU", params.inputs.primarySku.trim()),
|
||
...collect("secondary", "搭配SKU", params.inputs.secondarySku.trim()),
|
||
...(params.selectedBrandModel?.imageUrl
|
||
? [
|
||
{
|
||
id: params.selectedBrandModel.id,
|
||
source: "brandModel" as const,
|
||
sourceLabel: "品牌模特",
|
||
label: params.selectedBrandModel.displayName || params.selectedBrandModel.brandName,
|
||
name: params.selectedBrandModel.displayName || params.selectedBrandModel.brandName,
|
||
url: params.selectedBrandModel.imageUrl,
|
||
assetId: params.selectedBrandModel.assetId || null,
|
||
mimeType: params.selectedBrandModel.mimeType || null,
|
||
},
|
||
]
|
||
: []),
|
||
];
|
||
}
|
||
|
||
export function getAgentWorkflowProgress(params: {
|
||
config?: AgentWorkflowUiConfig | null;
|
||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||
}) {
|
||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||
const enabledSteps = flattenVisibleAgentWorkflowSteps(
|
||
getEnabledAgentWorkflowStepTree(config.steps || []),
|
||
params.selections
|
||
)
|
||
.map(({ step }) => step)
|
||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||
const completedSteps = enabledSteps.filter((step) => {
|
||
return isAgentWorkflowStepSelectionComplete({
|
||
step,
|
||
selectionValue: params.selections[step.key],
|
||
});
|
||
});
|
||
|
||
return {
|
||
total: enabledSteps.length,
|
||
completed: completedSteps.length,
|
||
isComplete: enabledSteps.length > 0 && completedSteps.length === enabledSteps.length,
|
||
};
|
||
}
|