783 lines
27 KiB
TypeScript
783 lines
27 KiB
TypeScript
import type {
|
||
AgentWorkflowBrandStoreEntry,
|
||
AgentWorkflowOptionUiConfig,
|
||
AgentWorkflowStepKey,
|
||
AgentWorkflowStepUiConfig,
|
||
AgentWorkflowUiConfig,
|
||
} from "@/types/agent";
|
||
|
||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 8;
|
||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 8;
|
||
|
||
export interface FlattenedAgentWorkflowStep {
|
||
step: AgentWorkflowStepUiConfig;
|
||
depth: number;
|
||
parentKeys: string[];
|
||
}
|
||
|
||
function cloneOption(
|
||
option: AgentWorkflowOptionUiConfig
|
||
): AgentWorkflowOptionUiConfig {
|
||
return {
|
||
...option,
|
||
description: option.description || "",
|
||
previewImageUrl: option.previewImageUrl || "",
|
||
promptFragment: option.promptFragment || "",
|
||
blockedOptionKeys: Array.from(new Set((option.blockedOptionKeys || []).filter(Boolean))).slice(
|
||
0,
|
||
100
|
||
),
|
||
children: (option.children || []).map(cloneOption),
|
||
};
|
||
}
|
||
|
||
function cloneStep(step: AgentWorkflowStepUiConfig): AgentWorkflowStepUiConfig {
|
||
return {
|
||
...step,
|
||
description: step.description || "",
|
||
options: step.options.map(cloneOption),
|
||
children: (step.children || []).map(cloneStep),
|
||
};
|
||
}
|
||
|
||
function createStep(
|
||
key: string,
|
||
title: string,
|
||
description: string,
|
||
options: AgentWorkflowOptionUiConfig[]
|
||
): AgentWorkflowStepUiConfig {
|
||
return {
|
||
key,
|
||
title,
|
||
description,
|
||
enabled: true,
|
||
sortOrder: 0,
|
||
options,
|
||
children: [],
|
||
};
|
||
}
|
||
|
||
function createOption(
|
||
key: string,
|
||
buttonName: string,
|
||
promptFragment: string,
|
||
sortOrder: number
|
||
): AgentWorkflowOptionUiConfig {
|
||
return {
|
||
key,
|
||
buttonName,
|
||
description: "",
|
||
previewImageUrl: "",
|
||
enabled: true,
|
||
sortOrder,
|
||
promptFragment,
|
||
blockedOptionKeys: [],
|
||
children: [],
|
||
};
|
||
}
|
||
|
||
const defaultSteps: AgentWorkflowStepUiConfig[] = [
|
||
createStep("audience", "人群选择", "先确认目标家庭结构与生活状态。", [
|
||
createOption("single_youth", "独居青年", "面向独居青年,强调轻盈、效率感和个人表达。", 0),
|
||
createOption("young_couple", "年轻情侣", "面向年轻情侣,强调陪伴感、松弛感和高颜值日常。", 1),
|
||
createOption("midlife_no_kids", "中年无孩夫妻", "面向中年无孩夫妻,强调稳定、质感与舒适收纳。", 2),
|
||
createOption("family_with_kids", "有孩家庭", "面向有孩家庭,强调互动、耐用、安全与成长场景。", 3),
|
||
createOption("senior_couple", "老年夫妻", "面向老年夫妻,强调便利、温暖、低刺激与安心动线。", 4),
|
||
]),
|
||
createStep("style", "主题风格", "确定空间视觉主风格。", [
|
||
createOption("japanese", "日系风", "整体风格为日系,强调自然木色、留白与温和秩序。", 0),
|
||
createOption("new_chinese", "新中式", "整体风格为新中式,强调东方韵味与现代克制。", 1),
|
||
createOption("american", "美式", "整体风格为美式,强调舒展体量、生活感与复古质感。", 2),
|
||
createOption("healing", "治愈风", "整体风格为治愈风,强调柔和、放松与亲和力。", 3),
|
||
createOption("modern", "现代风", "整体风格为现代风,强调简洁、利落与当代感。", 4),
|
||
createOption("eclectic", "混搭风", "整体风格为混搭风,强调层次变化与个性搭配。", 5),
|
||
createOption("vintage", "中古风", "整体风格为中古风,强调复古家具与时间感。", 6),
|
||
createOption("cream", "奶油风", "整体风格为奶油风,强调柔光、低饱和与绵密材质。", 7),
|
||
]),
|
||
createStep("space", "空间选择", "确定主要空间。", [
|
||
createOption("living_room", "客厅", "主空间为客厅。", 0),
|
||
createOption("kitchen", "厨房", "主空间为厨房。", 1),
|
||
createOption("bedroom", "卧室", "主空间为卧室。", 2),
|
||
createOption("dining_room", "餐厅", "主空间为餐厅。", 3),
|
||
createOption("study", "书房", "主空间为书房。", 4),
|
||
createOption("foyer", "门厅", "主空间为门厅。", 5),
|
||
createOption("balcony", "阳台", "主空间为阳台。", 6),
|
||
createOption("bathroom", "卫生间", "主空间为卫生间。", 7),
|
||
]),
|
||
createStep("subSpace", "子空间选择", "确定重点表现区域。", [
|
||
createOption("tv_console", "电视柜空间", "重点表现电视柜空间与背景墙关系。", 0),
|
||
createOption("sofa_area", "沙发空间", "重点表现沙发空间与会客氛围。", 1),
|
||
createOption("dining_table", "餐桌空间", "重点表现餐桌空间与就餐动线。", 2),
|
||
createOption("bedside_area", "床头空间", "重点表现床头空间与睡眠氛围。", 3),
|
||
createOption("island_area", "岛台空间", "重点表现岛台空间与操作效率。", 4),
|
||
createOption("desk_area", "书桌空间", "重点表现书桌空间与专注氛围。", 5),
|
||
]),
|
||
createStep("hardDecoration", "硬装方案", "确定空间硬装基调。", [
|
||
createOption("hard_plan_1", "硬装方案1", "硬装采用方案1,整体偏克制基础款。", 0),
|
||
createOption("hard_plan_2", "硬装方案2", "硬装采用方案2,整体偏精致层次款。", 1),
|
||
createOption("hard_plan_3", "硬装方案3", "硬装采用方案3,整体偏视觉强化款。", 2),
|
||
]),
|
||
createStep("softDecoration", "软装方案", "确定软装搭配方向。", [
|
||
createOption("soft_plan_1", "软装方案1", "软装采用方案1,整体偏稳妥耐看。", 0),
|
||
createOption("soft_plan_2", "软装方案2", "软装采用方案2,整体偏生活感与柔和搭配。", 1),
|
||
createOption("soft_plan_3", "软装方案3", "软装采用方案3,整体偏高辨识度和氛围表达。", 2),
|
||
]),
|
||
createStep("lighting", "光氛选择", "确定光照时段与氛围。", [
|
||
createOption("natural_daylight", "自然阳光", "光照氛围为自然阳光,通透明亮。", 0),
|
||
createOption("warm_afternoon", "暖调午后", "光照氛围为暖调午后,柔和松弛。", 1),
|
||
createOption("romantic_night", "浪漫夜晚", "光照氛围为浪漫夜晚,暖光层次分明。", 2),
|
||
]),
|
||
createStep("accessories", "配饰陈列", "确定配饰收口与陈列方式。", [
|
||
createOption("accessories_a", "配饰方案A", "配饰采用方案A,强调克制点缀。", 0),
|
||
createOption("accessories_b", "配饰方案B", "配饰采用方案B,强调生活化陈列。", 1),
|
||
createOption("accessories_c", "配饰方案C", "配饰采用方案C,强调高完成度展示。", 2),
|
||
]),
|
||
];
|
||
|
||
export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
||
inputOptions: {
|
||
brands: [],
|
||
stores: [],
|
||
priceBands: [],
|
||
brandStoreEntries: [],
|
||
},
|
||
steps: defaultSteps.map((step, index) =>
|
||
cloneStep({
|
||
...step,
|
||
sortOrder: index,
|
||
})
|
||
),
|
||
};
|
||
|
||
export function flattenAgentWorkflowSteps(
|
||
steps: AgentWorkflowStepUiConfig[],
|
||
depth = 1,
|
||
parentKeys: string[] = []
|
||
): FlattenedAgentWorkflowStep[] {
|
||
return steps.flatMap((step) => [
|
||
{
|
||
step,
|
||
depth,
|
||
parentKeys,
|
||
},
|
||
...flattenAgentWorkflowSteps(step.children || [], depth + 1, [...parentKeys, step.key]),
|
||
]);
|
||
}
|
||
|
||
export function flattenVisibleAgentWorkflowSteps(
|
||
steps: AgentWorkflowStepUiConfig[],
|
||
selections: Partial<Record<string, string>>,
|
||
depth = 1,
|
||
parentKeys: string[] = [],
|
||
ancestorsSatisfied = true
|
||
): FlattenedAgentWorkflowStep[] {
|
||
return [...steps]
|
||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||
.flatMap((step) => {
|
||
const normalizedStep = {
|
||
...step,
|
||
options: [...(step.options || [])].sort((left, right) => left.sortOrder - right.sortOrder),
|
||
children: [...(step.children || [])].sort((left, right) => left.sortOrder - right.sortOrder),
|
||
};
|
||
|
||
if (!ancestorsSatisfied) {
|
||
return [] as FlattenedAgentWorkflowStep[];
|
||
}
|
||
|
||
const currentEntry: FlattenedAgentWorkflowStep = {
|
||
step: normalizedStep,
|
||
depth,
|
||
parentKeys,
|
||
};
|
||
const currentSatisfied =
|
||
normalizedStep.options.length === 0 || Boolean(String(selections[normalizedStep.key] || "").trim());
|
||
|
||
return [
|
||
currentEntry,
|
||
...flattenVisibleAgentWorkflowSteps(
|
||
normalizedStep.children,
|
||
selections,
|
||
depth + 1,
|
||
[...parentKeys, normalizedStep.key],
|
||
currentSatisfied
|
||
),
|
||
];
|
||
});
|
||
}
|
||
|
||
export function findAgentWorkflowStepByKey(
|
||
steps: AgentWorkflowStepUiConfig[],
|
||
stepKey: string
|
||
): AgentWorkflowStepUiConfig | null {
|
||
for (const step of steps) {
|
||
if (step.key === stepKey) {
|
||
return step;
|
||
}
|
||
const foundChild = findAgentWorkflowStepByKey(step.children || [], stepKey);
|
||
if (foundChild) {
|
||
return foundChild;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function countAgentWorkflowSteps(steps: AgentWorkflowStepUiConfig[]): number {
|
||
return flattenAgentWorkflowSteps(steps).length;
|
||
}
|
||
|
||
export function countAgentWorkflowOptions(steps: AgentWorkflowStepUiConfig[]): number {
|
||
return flattenAgentWorkflowSteps(steps).reduce(
|
||
(total, item) =>
|
||
total +
|
||
flattenAgentWorkflowOptionTree(item.step.options || []).length,
|
||
0
|
||
);
|
||
}
|
||
|
||
export function normalizeAgentWorkflowStepOrdering(
|
||
steps: AgentWorkflowStepUiConfig[]
|
||
): AgentWorkflowStepUiConfig[] {
|
||
return steps.map((step, stepIndex) => ({
|
||
...step,
|
||
sortOrder: stepIndex,
|
||
options: normalizeAgentWorkflowOptionOrdering(step.options || []),
|
||
children: normalizeAgentWorkflowStepOrdering(step.children || []),
|
||
}));
|
||
}
|
||
|
||
export function getEnabledAgentWorkflowStepTree(
|
||
steps: AgentWorkflowStepUiConfig[]
|
||
): AgentWorkflowStepUiConfig[] {
|
||
return [...steps]
|
||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||
.filter((step) => step.enabled !== false)
|
||
.map((step) => ({
|
||
...step,
|
||
options: getEnabledAgentWorkflowOptionTree(step.options || []),
|
||
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
||
}))
|
||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
||
}
|
||
|
||
export interface FlattenedAgentWorkflowOption {
|
||
option: AgentWorkflowOptionUiConfig;
|
||
depth: number;
|
||
parentKeys: string[];
|
||
}
|
||
|
||
export function flattenAgentWorkflowOptionTree(
|
||
options: AgentWorkflowOptionUiConfig[],
|
||
depth = 1,
|
||
parentKeys: string[] = []
|
||
): FlattenedAgentWorkflowOption[] {
|
||
return options.flatMap((option) => [
|
||
{
|
||
option,
|
||
depth,
|
||
parentKeys,
|
||
},
|
||
...flattenAgentWorkflowOptionTree(
|
||
option.children || [],
|
||
depth + 1,
|
||
[...parentKeys, option.key]
|
||
),
|
||
]);
|
||
}
|
||
|
||
export function countAgentWorkflowOptionTree(
|
||
options: AgentWorkflowOptionUiConfig[]
|
||
): number {
|
||
return flattenAgentWorkflowOptionTree(options).length;
|
||
}
|
||
|
||
function collectWorkflowOptionKeySetFromSteps(
|
||
steps: AgentWorkflowStepUiConfig[]
|
||
): Set<string> {
|
||
return new Set(
|
||
flattenAgentWorkflowSteps(steps).flatMap(({ step }) =>
|
||
flattenAgentWorkflowOptionTree(step.options || []).map(({ option }) => option.key)
|
||
)
|
||
);
|
||
}
|
||
|
||
export function normalizeAgentWorkflowOptionOrdering(
|
||
options: AgentWorkflowOptionUiConfig[]
|
||
): AgentWorkflowOptionUiConfig[] {
|
||
return options.map((option, optionIndex) => ({
|
||
...option,
|
||
sortOrder: optionIndex,
|
||
children: normalizeAgentWorkflowOptionOrdering(option.children || []),
|
||
}));
|
||
}
|
||
|
||
export function getEnabledAgentWorkflowOptionTree(
|
||
options: AgentWorkflowOptionUiConfig[]
|
||
): AgentWorkflowOptionUiConfig[] {
|
||
return [...options]
|
||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||
.filter((option) => option.enabled !== false)
|
||
.map((option) => ({
|
||
...option,
|
||
children: getEnabledAgentWorkflowOptionTree(option.children || []),
|
||
}));
|
||
}
|
||
|
||
export function findAgentWorkflowOptionByKey(
|
||
options: AgentWorkflowOptionUiConfig[],
|
||
optionKey: string
|
||
): AgentWorkflowOptionUiConfig | null {
|
||
for (const option of options) {
|
||
if (option.key === optionKey) {
|
||
return option;
|
||
}
|
||
const foundChild = findAgentWorkflowOptionByKey(option.children || [], optionKey);
|
||
if (foundChild) {
|
||
return foundChild;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function normalizeText(value: unknown, maxLength: number) {
|
||
return String(value || "").trim().slice(0, maxLength);
|
||
}
|
||
|
||
function normalizeKey(value: unknown, fallback: string) {
|
||
const normalized = String(value || "")
|
||
.trim()
|
||
.slice(0, 64)
|
||
.replace(/\s+/g, "_")
|
||
.replace(/[^\w-]/g, "_")
|
||
.replace(/_+/g, "_")
|
||
.replace(/^_+|_+$/g, "")
|
||
.toLowerCase();
|
||
return normalized || fallback;
|
||
}
|
||
|
||
function normalizeBoolean(value: unknown, fallback: boolean) {
|
||
return typeof value === "boolean" ? value : fallback;
|
||
}
|
||
|
||
function normalizeNumber(value: unknown, fallback: number, max = 999) {
|
||
const parsed = Number(value);
|
||
if (!Number.isFinite(parsed)) return fallback;
|
||
return Math.max(0, Math.min(max, Math.round(parsed)));
|
||
}
|
||
|
||
function normalizeKeyList(value: unknown, max = 100) {
|
||
if (!Array.isArray(value)) return [] as string[];
|
||
return Array.from(
|
||
new Set(
|
||
value
|
||
.map((item, index) => normalizeKey(item, `blocked_option_${index + 1}`))
|
||
.filter(Boolean)
|
||
.slice(0, max)
|
||
)
|
||
);
|
||
}
|
||
|
||
function sanitizeBlockedOptionTree(
|
||
options: AgentWorkflowOptionUiConfig[],
|
||
validKeys = new Set(
|
||
flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key)
|
||
)
|
||
): AgentWorkflowOptionUiConfig[] {
|
||
return options.map((option) => ({
|
||
...option,
|
||
blockedOptionKeys: Array.from(
|
||
new Set(
|
||
(option.blockedOptionKeys || []).filter(
|
||
(blockedKey) => blockedKey !== option.key && validKeys.has(blockedKey)
|
||
)
|
||
)
|
||
).slice(0, 100),
|
||
children: sanitizeBlockedOptionTree(option.children || [], validKeys),
|
||
}));
|
||
}
|
||
|
||
function sanitizeBlockedWorkflowStepTree(
|
||
steps: AgentWorkflowStepUiConfig[],
|
||
validKeys: Set<string>
|
||
): AgentWorkflowStepUiConfig[] {
|
||
return steps.map((step) => ({
|
||
...step,
|
||
options: sanitizeBlockedOptionTree(step.options || [], validKeys),
|
||
children: sanitizeBlockedWorkflowStepTree(step.children || [], validKeys),
|
||
}));
|
||
}
|
||
|
||
export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiConfig {
|
||
const raw =
|
||
value && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: {};
|
||
const hasRawSteps = Array.isArray(raw.steps);
|
||
const rawSteps = Array.isArray(raw.steps) ? raw.steps : [];
|
||
const normalizeStringList = (input: unknown) => {
|
||
if (!Array.isArray(input)) return [] as string[];
|
||
return Array.from(
|
||
new Set(
|
||
input
|
||
.map((item) => normalizeText(item, 64))
|
||
.filter(Boolean)
|
||
.slice(0, 300)
|
||
)
|
||
);
|
||
};
|
||
const rawInputOptions =
|
||
raw.inputOptions && typeof raw.inputOptions === "object" && !Array.isArray(raw.inputOptions)
|
||
? (raw.inputOptions as Record<string, unknown>)
|
||
: {};
|
||
const normalizeBrandStoreEntries = (input: unknown) => {
|
||
if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[];
|
||
|
||
const mappedEntries: Array<AgentWorkflowBrandStoreEntry | null> = input.map((item, index) => {
|
||
const record =
|
||
item && typeof item === "object" && !Array.isArray(item)
|
||
? (item as Record<string, unknown>)
|
||
: {};
|
||
const brandName = normalizeText(record.brandName, 64);
|
||
if (!brandName) return null;
|
||
const stores = normalizeStringList(record.stores).slice(0, 300);
|
||
|
||
return {
|
||
brandName,
|
||
stores,
|
||
sortOrder: normalizeNumber(record.sortOrder, index),
|
||
};
|
||
});
|
||
|
||
return mappedEntries
|
||
.filter((item): item is AgentWorkflowBrandStoreEntry => item !== null)
|
||
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
|
||
.map((entry, index) => ({
|
||
...entry,
|
||
sortOrder: index,
|
||
}));
|
||
};
|
||
const normalizedBrandStoreEntries = normalizeBrandStoreEntries(
|
||
rawInputOptions.brandStoreEntries
|
||
);
|
||
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
|
||
const normalizedStores = normalizeStringList(rawInputOptions.stores);
|
||
const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands);
|
||
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
|
||
normalizedBrandStoreEntries.length > 0
|
||
? normalizedBrandStoreEntries
|
||
: normalizedBrands.map((brandName, index) => ({
|
||
brandName,
|
||
stores: normalizedBrands.length === 1 ? normalizedStores : [],
|
||
sortOrder: index,
|
||
}));
|
||
const defaultStepByKey = new Map(
|
||
flattenAgentWorkflowSteps(defaultAgentWorkflowUiConfig.steps).map(({ step }) => [
|
||
step.key,
|
||
step,
|
||
] as const)
|
||
);
|
||
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||
const usedStepKeys = new Set<string>();
|
||
|
||
const normalizeOption = (
|
||
item: unknown,
|
||
optionIndex: number,
|
||
stepKey: string,
|
||
usedOptionKeys: Set<string>,
|
||
defaultOptions: AgentWorkflowOptionUiConfig[],
|
||
depth: number
|
||
): AgentWorkflowOptionUiConfig => {
|
||
const optionRecord =
|
||
item && typeof item === "object" && !Array.isArray(item)
|
||
? (item as Record<string, unknown>)
|
||
: {};
|
||
const defaultOption = defaultOptions.find(
|
||
(defaultItem) => defaultItem.key === normalizeText(optionRecord.key, 64)
|
||
);
|
||
const optionBaseKey = normalizeKey(
|
||
optionRecord.key,
|
||
defaultOption?.key || `${stepKey}_option_${optionIndex + 1}`
|
||
);
|
||
let optionKey = optionBaseKey;
|
||
let optionDuplicateIndex = 2;
|
||
while (usedOptionKeys.has(optionKey)) {
|
||
optionKey = `${optionBaseKey}_${optionDuplicateIndex}`;
|
||
optionDuplicateIndex += 1;
|
||
}
|
||
usedOptionKeys.add(optionKey);
|
||
|
||
const buttonName =
|
||
normalizeText(optionRecord.buttonName, 64) ||
|
||
defaultOption?.buttonName ||
|
||
`选项${optionIndex + 1}`;
|
||
const promptFragment =
|
||
normalizeText(optionRecord.promptFragment, 1000) ||
|
||
defaultOption?.promptFragment ||
|
||
buttonName;
|
||
const hasRawBlockedOptionKeys = Array.isArray(optionRecord.blockedOptionKeys);
|
||
const hasRawChildren = Array.isArray(optionRecord.children);
|
||
const rawChildren = hasRawChildren ? (optionRecord.children as unknown[]) : [];
|
||
const defaultChildren =
|
||
!hasRawChildren && defaultOption?.children ? defaultOption.children : [];
|
||
const childSource =
|
||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH && (rawChildren.length > 0 || hasRawChildren)
|
||
? rawChildren
|
||
: depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||
? defaultChildren
|
||
: [];
|
||
|
||
return {
|
||
key: optionKey,
|
||
buttonName,
|
||
description:
|
||
normalizeText(optionRecord.description, 300) || defaultOption?.description || "",
|
||
previewImageUrl: normalizeText(optionRecord.previewImageUrl, 1000),
|
||
enabled: normalizeBoolean(optionRecord.enabled, defaultOption?.enabled ?? true),
|
||
sortOrder: normalizeNumber(
|
||
optionRecord.sortOrder,
|
||
defaultOption?.sortOrder ?? optionIndex
|
||
),
|
||
promptFragment,
|
||
blockedOptionKeys: hasRawBlockedOptionKeys
|
||
? normalizeKeyList(optionRecord.blockedOptionKeys, 100)
|
||
: normalizeKeyList(defaultOption?.blockedOptionKeys, 100),
|
||
children:
|
||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||
? childSource
|
||
.map((child, childIndex) =>
|
||
normalizeOption(
|
||
child,
|
||
childIndex,
|
||
optionKey,
|
||
usedOptionKeys,
|
||
defaultOption?.children || [],
|
||
depth + 1
|
||
)
|
||
)
|
||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||
: [],
|
||
};
|
||
};
|
||
|
||
const normalizeStep = (
|
||
item: unknown,
|
||
stepIndex: number,
|
||
depth: number
|
||
): AgentWorkflowStepUiConfig => {
|
||
const stepRecord =
|
||
item && typeof item === "object" && !Array.isArray(item)
|
||
? (item as Record<string, unknown>)
|
||
: null;
|
||
const candidateKey = normalizeKey(stepRecord?.key, `step_${stepIndex + 1}`);
|
||
let stepKey = candidateKey;
|
||
let duplicateIndex = 2;
|
||
while (usedStepKeys.has(stepKey)) {
|
||
stepKey = `${candidateKey}_${duplicateIndex}`;
|
||
duplicateIndex += 1;
|
||
}
|
||
usedStepKeys.add(stepKey);
|
||
|
||
const defaultStep = defaultStepByKey.get(stepKey);
|
||
const fallbackTitle = defaultStep?.title || `步骤${stepIndex + 1}`;
|
||
const fallbackDescription = defaultStep?.description || "";
|
||
const hasRawOptions = Array.isArray(stepRecord?.options);
|
||
const rawOptions = hasRawOptions ? (stepRecord?.options as unknown[]) : [];
|
||
const defaultOptions = !hasRawOptions && defaultStep?.options ? defaultStep.options : [];
|
||
const optionSource = rawOptions.length > 0 || hasRawOptions ? rawOptions : defaultOptions;
|
||
const usedOptionKeys = new Set<string>();
|
||
|
||
const options = optionSource
|
||
.map((option, optionIndex) =>
|
||
normalizeOption(option, optionIndex, stepKey, usedOptionKeys, defaultOptions, 1)
|
||
)
|
||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||
const hasRawChildren = Array.isArray(stepRecord?.children);
|
||
const rawChildren = hasRawChildren ? (stepRecord?.children as unknown[]) : [];
|
||
const defaultChildren =
|
||
!hasRawChildren && defaultStep?.children ? defaultStep.children : [];
|
||
const childSource =
|
||
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH && (rawChildren.length > 0 || hasRawChildren)
|
||
? rawChildren
|
||
: depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||
? defaultChildren
|
||
: [];
|
||
|
||
return {
|
||
key: stepKey,
|
||
title: normalizeText(stepRecord?.title, 64) || fallbackTitle,
|
||
description: normalizeText(stepRecord?.description, 300) || fallbackDescription,
|
||
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
|
||
sortOrder: normalizeNumber(stepRecord?.sortOrder, defaultStep?.sortOrder ?? stepIndex),
|
||
options,
|
||
children:
|
||
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||
? childSource
|
||
.map((child, childIndex) => normalizeStep(child, childIndex, depth + 1))
|
||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||
: [],
|
||
};
|
||
};
|
||
|
||
const normalizedSteps = sourceSteps
|
||
.map((item, stepIndex) => normalizeStep(item, stepIndex, 1))
|
||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||
const validOptionKeys = collectWorkflowOptionKeySetFromSteps(normalizedSteps);
|
||
const sanitizedSteps = sanitizeBlockedWorkflowStepTree(normalizedSteps, validOptionKeys);
|
||
|
||
return {
|
||
inputOptions: {
|
||
brands:
|
||
fallbackBrandStoreEntries.length > 0
|
||
? fallbackBrandStoreEntries.map((entry) => entry.brandName)
|
||
: normalizedBrands,
|
||
stores:
|
||
fallbackBrandStoreEntries.length > 0
|
||
? Array.from(
|
||
new Set(fallbackBrandStoreEntries.flatMap((entry) => entry.stores))
|
||
)
|
||
: normalizedStores,
|
||
priceBands: normalizedPriceBands,
|
||
brandStoreEntries: fallbackBrandStoreEntries,
|
||
},
|
||
steps: sanitizedSteps,
|
||
};
|
||
}
|
||
|
||
export function getAgentWorkflowBrandStoreEntries(config?: AgentWorkflowUiConfig | null) {
|
||
return config?.inputOptions?.brandStoreEntries || defaultAgentWorkflowUiConfig.inputOptions?.brandStoreEntries || [];
|
||
}
|
||
|
||
export function getAgentWorkflowStoreOptionsByBrand(params: {
|
||
config?: AgentWorkflowUiConfig | null;
|
||
brand?: string | null;
|
||
}) {
|
||
const entries = getAgentWorkflowBrandStoreEntries(params.config);
|
||
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
|
||
if (!normalizedBrand) {
|
||
return [];
|
||
}
|
||
|
||
const matchedEntry = entries.find(
|
||
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
|
||
);
|
||
if (matchedEntry && matchedEntry.stores.length > 0) {
|
||
return matchedEntry.stores;
|
||
}
|
||
|
||
return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [];
|
||
}
|
||
|
||
export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) {
|
||
return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [];
|
||
}
|
||
|
||
export function parseAgentWorkflowSelectionPath(value?: string | null) {
|
||
return String(value || "")
|
||
.split(">")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
.slice(0, AGENT_WORKFLOW_MAX_OPTION_DEPTH);
|
||
}
|
||
|
||
export function serializeAgentWorkflowSelectionPath(optionKeys: string[]) {
|
||
return optionKeys.filter(Boolean).slice(0, AGENT_WORKFLOW_MAX_OPTION_DEPTH).join(">");
|
||
}
|
||
|
||
export function getAgentWorkflowSelectedOptionPath(params: {
|
||
step: AgentWorkflowStepUiConfig;
|
||
selectionValue?: string | null;
|
||
}) {
|
||
const keys = parseAgentWorkflowSelectionPath(params.selectionValue);
|
||
const selectedOptions: AgentWorkflowOptionUiConfig[] = [];
|
||
let currentOptions = params.step.options || [];
|
||
|
||
for (const key of keys) {
|
||
const matchedOption = currentOptions.find((option) => option.key === key);
|
||
if (!matchedOption) break;
|
||
selectedOptions.push(matchedOption);
|
||
currentOptions = matchedOption.children || [];
|
||
}
|
||
|
||
return selectedOptions;
|
||
}
|
||
|
||
export function sanitizeAgentWorkflowSelections(params: {
|
||
config?: AgentWorkflowUiConfig | null;
|
||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||
}) {
|
||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||
const enabledSteps = flattenAgentWorkflowSteps(
|
||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
|
||
).map(({ step }) => step);
|
||
|
||
const sanitized: Partial<Record<AgentWorkflowStepKey, string>> = {};
|
||
|
||
enabledSteps.forEach((step) => {
|
||
const rawSelectionValue = String(params.selections[step.key] || "").trim();
|
||
if (!rawSelectionValue) {
|
||
return;
|
||
}
|
||
|
||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||
step,
|
||
selectionValue: rawSelectionValue,
|
||
});
|
||
if (selectedOptions.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const serialized = serializeAgentWorkflowSelectionPath(
|
||
selectedOptions.map((option) => option.key)
|
||
);
|
||
if (serialized) {
|
||
sanitized[step.key] = serialized;
|
||
}
|
||
});
|
||
|
||
return sanitized;
|
||
}
|
||
|
||
export function getAgentWorkflowBlockedOptionKeys(params: {
|
||
step: AgentWorkflowStepUiConfig;
|
||
selectionValue?: string | null;
|
||
}) {
|
||
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
|
||
return Array.from(
|
||
new Set(
|
||
selectedOptions.flatMap((option) => (option.blockedOptionKeys || []).filter(Boolean))
|
||
)
|
||
);
|
||
}
|
||
|
||
export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||
config?: AgentWorkflowUiConfig | null;
|
||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||
}) {
|
||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||
const visibleSteps = flattenVisibleAgentWorkflowSteps(
|
||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||
params.selections
|
||
).map(({ step }) => step);
|
||
|
||
return Array.from(
|
||
new Set(
|
||
visibleSteps.flatMap((step) =>
|
||
getAgentWorkflowBlockedOptionKeys({
|
||
step,
|
||
selectionValue: params.selections[step.key],
|
||
})
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
export function isAgentWorkflowStepSelectionComplete(params: {
|
||
step: AgentWorkflowStepUiConfig;
|
||
selectionValue?: string | null;
|
||
}) {
|
||
const selectedPath = getAgentWorkflowSelectedOptionPath(params);
|
||
if (selectedPath.length === 0) return false;
|
||
const lastOption = selectedPath[selectedPath.length - 1];
|
||
return getEnabledAgentWorkflowOptionTree(lastOption.children || []).length === 0;
|
||
}
|