ai-chatbot/lib/agents/workflow-composer.ts
Your Name 7eda4720d3
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: extend workflow category and card controls
2026-04-02 12:35:24 +08:00

459 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
thirdCategory: 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 buildBundleUsagePromptLine(params: {
referenceImages: AgentWorkflowReferenceImage[];
}) {
const primaryCount = params.referenceImages.filter((item) => item.source === "primary").length;
const secondaryCount = params.referenceImages.filter((item) => item.source === "secondary").length;
if (primaryCount <= 0 || secondaryCount <= 0) {
return "";
}
return "搭配SKU使用规则:搭配SKU是独立的第二产品,不是主SKU的另一角度或替代图。需要做搭配展示时,必须同场景同时展示主SKU与搭配SKU(例如桌子+椅子),保持两者各自的外观、比例和结构,不得把两件产品融合成一件,也不得用搭配SKU替换主SKU。";
}
function normalizeWorkflowText(value: string) {
return String(value || "")
.replace(/\s+/g, " ")
.trim();
}
function buildGroupedSelectionLines(params: {
selections: AgentWorkflowComposerSelection[];
valueKey: "optionName" | "promptFragment";
separator: string;
leafOnly?: boolean;
dedupeAcrossSteps?: boolean;
}) {
const grouped = new Map<
string,
{
stepTitle: string;
values: string[];
valueSet: Set<string>;
}
>();
const globalValueSet = new Set<string>();
const sourceSelections = params.leafOnly
? params.selections.filter((item, index, items) => {
const nextItem = items[index + 1];
return !nextItem || nextItem.stepKey !== item.stepKey;
})
: params.selections;
sourceSelections.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 (params.dedupeAcrossSteps && globalValueSet.has(dedupeKey)) {
return;
}
if (!currentGroup.valueSet.has(dedupeKey)) {
currentGroup.valueSet.add(dedupeKey);
if (params.dedupeAcrossSteps) {
globalValueSet.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.thirdCategory.trim()) {
summaryParts.push(`三级分类:${params.inputs.thirdCategory.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.thirdCategory.trim()) {
promptLines.push(`三级分类:${params.inputs.thirdCategory.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("、")})` : ""
}`
);
}
const bundleUsagePrompt = buildBundleUsagePromptLine({ referenceImages });
if (bundleUsagePrompt) {
promptLines.push(bundleUsagePrompt);
}
promptLines.push(
...buildGroupedSelectionLines({
selections,
valueKey: "promptFragment",
separator: ";",
dedupeAcrossSteps: true,
})
);
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,
thirdCategory: params.inputs.thirdCategory.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,
};
}