import type { AgentWorkflowBrandStoreEntry, AgentWorkflowCategoryLabels, AgentWorkflowConstraintSettings, AgentWorkflowFourthCategoryEntry, AgentWorkflowOptionUiConfig, AgentWorkflowStoreEntry, AgentWorkflowThirdCategoryEntry, 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 const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = { firstCategory: "品牌", secondCategory: "店铺", thirdCategory: "三级分类", fourthCategory: "四级分类", fifthCategory: "五级分类", }; export const defaultAgentWorkflowConstraintSettings: AgentWorkflowConstraintSettings = { crossLevelMutualExclusionEnabled: false, }; export interface AgentWorkflowBrandCategoryOption { value: string; level: "third" | "fourth" | "fifth"; levelLabel: string; } 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: [], categoryLabels: defaultAgentWorkflowCategoryLabels, thirdCategories: [], fourthCategories: [], fifthCategories: [], priceBands: [], brandStoreEntries: [], }, constraintSettings: defaultAgentWorkflowConstraintSettings, steps: defaultSteps.map((step, index) => cloneStep({ ...step, sortOrder: index, }) ), }; export function normalizeAgentWorkflowCategoryLabels( value: unknown ): AgentWorkflowCategoryLabels { const record = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const normalizeLabel = (candidate: unknown, fallback: string) => { const text = normalizeText(candidate, 24); return text || fallback; }; return { firstCategory: normalizeLabel( record.firstCategory ?? record.firstCategoryLabel ?? record.brand ?? record.brandLabel, defaultAgentWorkflowCategoryLabels.firstCategory ), secondCategory: normalizeLabel( record.secondCategory ?? record.secondCategoryLabel ?? record.store ?? record.storeLabel, defaultAgentWorkflowCategoryLabels.secondCategory ), thirdCategory: normalizeLabel( record.thirdCategory ?? record.thirdCategoryLabel, defaultAgentWorkflowCategoryLabels.thirdCategory ), fourthCategory: normalizeLabel( record.fourthCategory ?? record.fourthCategoryLabel, defaultAgentWorkflowCategoryLabels.fourthCategory ), fifthCategory: normalizeLabel( record.fifthCategory ?? record.fifthCategoryLabel, defaultAgentWorkflowCategoryLabels.fifthCategory ), }; } export function normalizeAgentWorkflowConstraintSettings( value: unknown ): AgentWorkflowConstraintSettings { const record = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; return { crossLevelMutualExclusionEnabled: normalizeBoolean( record.crossLevelMutualExclusionEnabled ?? record.enableCrossLevelMutualExclusion, defaultAgentWorkflowConstraintSettings.crossLevelMutualExclusionEnabled ), }; } 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>, 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 function getRenderableAgentWorkflowOptionTree( options: AgentWorkflowOptionUiConfig[] ): AgentWorkflowOptionUiConfig[] { return [...options] .sort((left, right) => left.sortOrder - right.sortOrder) .map((option) => ({ ...option, children: getRenderableAgentWorkflowOptionTree(option.children || []), })); } export function getRenderableAgentWorkflowStepTree( steps: AgentWorkflowStepUiConfig[] ): AgentWorkflowStepUiConfig[] { return [...steps] .sort((left, right) => left.sortOrder - right.sortOrder) .filter((step) => step.enabled !== false) .map((step) => ({ ...step, options: getRenderableAgentWorkflowOptionTree(step.options || []), children: getRenderableAgentWorkflowStepTree(step.children || []), })) .filter( (step) => getEnabledAgentWorkflowOptionTree(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 { 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 ): 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) : {}; 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) : {}; const normalizeBrandStoreEntries = (input: unknown) => { if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[]; const normalizeFourthCategoryEntries = ( rawFourthCategoryEntries: unknown, legacyFourthCategories: string[] ): AgentWorkflowFourthCategoryEntry[] => { const mappedEntries: Array = Array.isArray( rawFourthCategoryEntries ) ? rawFourthCategoryEntries.map((fourthItem, fourthIndex) => { if (typeof fourthItem === "string") { const categoryName = normalizeText(fourthItem, 64); if (!categoryName) return null; return { categoryName, fifthCategories: [], sortOrder: fourthIndex, }; } const fourthRecord = fourthItem && typeof fourthItem === "object" && !Array.isArray(fourthItem) ? (fourthItem as Record) : {}; const categoryName = normalizeText( fourthRecord.categoryName ?? fourthRecord.fourthCategoryName, 64 ); if (!categoryName) return null; return { categoryName, fifthCategories: normalizeStringList(fourthRecord.fifthCategories).slice(0, 300), sortOrder: normalizeNumber(fourthRecord.sortOrder, fourthIndex), }; }) : []; const normalizedFromEntries = mappedEntries .filter((item): item is AgentWorkflowFourthCategoryEntry => item !== null) .sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)) .map((entry, index) => ({ ...entry, fifthCategories: Array.from( new Set((entry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), sortOrder: index, })); if (normalizedFromEntries.length > 0) { return normalizedFromEntries; } return legacyFourthCategories.map((categoryName, index) => ({ categoryName, fifthCategories: [], sortOrder: index, })); }; const normalizeThirdCategoryEntries = ( rawThirdCategoryEntries: unknown, legacyThirdCategories: string[] ): AgentWorkflowThirdCategoryEntry[] => { const mappedEntries: Array = Array.isArray( rawThirdCategoryEntries ) ? rawThirdCategoryEntries.map((thirdItem, thirdIndex) => { if (typeof thirdItem === "string") { const categoryName = normalizeText(thirdItem, 64); if (!categoryName) return null; return { categoryName, fourthCategories: [], fourthCategoryEntries: [], sortOrder: thirdIndex, }; } const thirdRecord = thirdItem && typeof thirdItem === "object" && !Array.isArray(thirdItem) ? (thirdItem as Record) : {}; const categoryName = normalizeText( thirdRecord.categoryName ?? thirdRecord.thirdCategoryName, 64 ); if (!categoryName) return null; const legacyFourthCategories = normalizeStringList(thirdRecord.fourthCategories).slice( 0, 300 ); const fourthCategoryEntries = normalizeFourthCategoryEntries( thirdRecord.fourthCategoryEntries, legacyFourthCategories ); return { categoryName, fourthCategories: fourthCategoryEntries.length > 0 ? fourthCategoryEntries.map((entry) => entry.categoryName) : legacyFourthCategories, fourthCategoryEntries, sortOrder: normalizeNumber(thirdRecord.sortOrder, thirdIndex), }; }) : []; const normalizedFromEntries = mappedEntries .filter((item): item is AgentWorkflowThirdCategoryEntry => item !== null) .sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)) .map((entry, index) => ({ ...entry, fourthCategories: Array.from( new Set((entry.fourthCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), fourthCategoryEntries: (entry.fourthCategoryEntries || []).map((fourthEntry, fourthIndex) => ({ ...fourthEntry, fifthCategories: Array.from( new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), sortOrder: fourthIndex, })), sortOrder: index, })); if (normalizedFromEntries.length > 0) { return normalizedFromEntries; } return legacyThirdCategories.map((categoryName, index) => ({ categoryName, fourthCategories: [], fourthCategoryEntries: [], sortOrder: index, })); }; const normalizeStoreEntries = ( rawStoreEntries: unknown, legacyStores: string[] ): AgentWorkflowStoreEntry[] => { const mappedEntries: Array = Array.isArray(rawStoreEntries) ? rawStoreEntries.map((storeItem, storeIndex) => { if (typeof storeItem === "string") { const storeName = normalizeText(storeItem, 64); if (!storeName) return null; return { storeName, thirdCategories: [], sortOrder: storeIndex, }; } const storeRecord = storeItem && typeof storeItem === "object" && !Array.isArray(storeItem) ? (storeItem as Record) : {}; const storeName = normalizeText(storeRecord.storeName, 64); if (!storeName) return null; const legacyThirdCategories = normalizeStringList(storeRecord.thirdCategories).slice( 0, 300 ); const thirdCategoryEntries = normalizeThirdCategoryEntries( storeRecord.thirdCategoryEntries, legacyThirdCategories ); return { storeName, thirdCategories: thirdCategoryEntries.length > 0 ? thirdCategoryEntries.map((entry) => entry.categoryName) : legacyThirdCategories, thirdCategoryEntries, sortOrder: normalizeNumber(storeRecord.sortOrder, storeIndex), }; }) : []; const normalizedFromEntries = mappedEntries .filter((item): item is AgentWorkflowStoreEntry => item !== null) .sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)) .map((entry, index) => ({ ...entry, thirdCategories: Array.from( new Set((entry.thirdCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), thirdCategoryEntries: (entry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({ ...thirdEntry, fourthCategories: Array.from( new Set((thirdEntry.fourthCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map( (fourthEntry, fourthIndex) => ({ ...fourthEntry, fifthCategories: Array.from( new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), sortOrder: fourthIndex, }) ), sortOrder: thirdIndex, })), sortOrder: index, })); if (normalizedFromEntries.length > 0) { return normalizedFromEntries; } return legacyStores.map((storeName, index) => ({ storeName, thirdCategories: [], thirdCategoryEntries: [], sortOrder: index, })); }; const mappedEntries: Array = input.map((item, index) => { const record = item && typeof item === "object" && !Array.isArray(item) ? (item as Record) : {}; const brandName = normalizeText(record.brandName, 64); if (!brandName) return null; const legacyStores = normalizeStringList(record.stores).slice(0, 300); const storeEntries = normalizeStoreEntries(record.storeEntries, legacyStores); const stores = storeEntries.map((entry) => entry.storeName); return { brandName, stores, storeEntries, 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 normalizedCategoryLabels = normalizeAgentWorkflowCategoryLabels( rawInputOptions.categoryLabels ?? rawInputOptions ); const normalizedBrands = normalizeStringList(rawInputOptions.brands); const normalizedStores = normalizeStringList(rawInputOptions.stores); const normalizedThirdCategories = normalizeStringList(rawInputOptions.thirdCategories); const normalizedFourthCategories = normalizeStringList(rawInputOptions.fourthCategories); const normalizedFifthCategories = normalizeStringList(rawInputOptions.fifthCategories); const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands); const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] = normalizedBrandStoreEntries.length > 0 ? normalizedBrandStoreEntries : normalizedBrands.map((brandName, index) => ({ brandName, stores: normalizedBrands.length === 1 ? normalizedStores : [], storeEntries: normalizedBrands.length === 1 ? normalizedStores.map((storeName, storeIndex) => ({ storeName, thirdCategories: [], thirdCategoryEntries: [], sortOrder: storeIndex, })) : [], 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(); const normalizeOption = ( item: unknown, optionIndex: number, stepKey: string, usedOptionKeys: Set, defaultOptions: AgentWorkflowOptionUiConfig[], depth: number ): AgentWorkflowOptionUiConfig => { const optionRecord = item && typeof item === "object" && !Array.isArray(item) ? (item as Record) : {}; 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) : 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(); 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.storeEntries || []).map((storeEntry) => storeEntry.storeName) ) ) ) : normalizedStores, thirdCategories: fallbackBrandStoreEntries.length > 0 ? Array.from( new Set( fallbackBrandStoreEntries.flatMap((entry) => (entry.storeEntries || []).flatMap( (storeEntry) => storeEntry.thirdCategories || [] ) ) ) ) : normalizedThirdCategories, fourthCategories: fallbackBrandStoreEntries.length > 0 ? Array.from( new Set( fallbackBrandStoreEntries.flatMap((entry) => (entry.storeEntries || []).flatMap((storeEntry) => (storeEntry.thirdCategoryEntries || []).flatMap( (thirdEntry) => thirdEntry.fourthCategories || [] ) ) ) ) ) : normalizedFourthCategories, fifthCategories: fallbackBrandStoreEntries.length > 0 ? Array.from( new Set( fallbackBrandStoreEntries.flatMap((entry) => (entry.storeEntries || []).flatMap((storeEntry) => (storeEntry.thirdCategoryEntries || []).flatMap((thirdEntry) => (thirdEntry.fourthCategoryEntries || []).flatMap( (fourthEntry) => fourthEntry.fifthCategories || [] ) ) ) ) ) ) : normalizedFifthCategories, categoryLabels: normalizedCategoryLabels, priceBands: normalizedPriceBands, brandStoreEntries: fallbackBrandStoreEntries, }, constraintSettings: normalizeAgentWorkflowConstraintSettings( raw.constraintSettings ?? { crossLevelMutualExclusionEnabled: raw.crossLevelMutualExclusionEnabled, } ), steps: sanitizedSteps, }; } export function getAgentWorkflowBrandStoreEntries(config?: AgentWorkflowUiConfig | null) { return config?.inputOptions?.brandStoreEntries || defaultAgentWorkflowUiConfig.inputOptions?.brandStoreEntries || []; } export function getAgentWorkflowCategoryLabels( config?: AgentWorkflowUiConfig | null ) { return normalizeAgentWorkflowCategoryLabels(config?.inputOptions?.categoryLabels); } export function getAgentWorkflowConstraintSettings( config?: AgentWorkflowUiConfig | null ) { return normalizeAgentWorkflowConstraintSettings(config?.constraintSettings); } export function getAgentWorkflowCategoryOptionsByBrand(params: { config?: AgentWorkflowUiConfig | null; brand?: string | null; }) { const entries = getAgentWorkflowBrandStoreEntries(params.config); const labels = getAgentWorkflowCategoryLabels(params.config); const normalizedBrand = String(params.brand || "").trim().toLowerCase(); const options: AgentWorkflowBrandCategoryOption[] = []; const seenValues = new Set(); const pushOption = ( value: string, level: AgentWorkflowBrandCategoryOption["level"], levelLabel: string ) => { const normalizedValue = String(value || "").trim(); if (!normalizedValue) { return; } const dedupeKey = normalizedValue.toLowerCase(); if (seenValues.has(dedupeKey)) { return; } seenValues.add(dedupeKey); options.push({ value: normalizedValue, level, levelLabel, }); }; if (normalizedBrand) { const matchedEntry = entries.find( (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand ); if (matchedEntry) { for (const storeEntry of matchedEntry.storeEntries || []) { for (const thirdEntry of storeEntry.thirdCategoryEntries || []) { pushOption(thirdEntry.categoryName, "third", labels.thirdCategory); for (const fourthEntry of thirdEntry.fourthCategoryEntries || []) { pushOption(fourthEntry.categoryName, "fourth", labels.fourthCategory); for (const fifthCategory of fourthEntry.fifthCategories || []) { pushOption(fifthCategory, "fifth", labels.fifthCategory); } } } } if (options.length > 0) { return options; } } } for (const thirdCategory of params.config?.inputOptions?.thirdCategories || []) { pushOption(thirdCategory, "third", labels.thirdCategory); } for (const fourthCategory of params.config?.inputOptions?.fourthCategories || []) { pushOption(fourthCategory, "fourth", labels.fourthCategory); } for (const fifthCategory of params.config?.inputOptions?.fifthCategories || []) { pushOption(fifthCategory, "fifth", labels.fifthCategory); } return options; } 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 ); const storeOptions = matchedEntry ? (matchedEntry.storeEntries || []).map((entry) => entry.storeName).filter(Boolean) : []; if (storeOptions.length > 0) { return storeOptions; } if (matchedEntry && matchedEntry.stores.length > 0) { return matchedEntry.stores; } return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || []; } export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: { config?: AgentWorkflowUiConfig | null; brand?: string | null; store?: string | null; }) { const entries = getAgentWorkflowBrandStoreEntries(params.config); const normalizedBrand = String(params.brand || "").trim().toLowerCase(); const normalizedStore = String(params.store || "").trim().toLowerCase(); if (!normalizedBrand || !normalizedStore) { return []; } const matchedEntry = entries.find( (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand ); const matchedStoreEntry = matchedEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore ); const thirdCategoryOptions = matchedStoreEntry?.thirdCategoryEntries && matchedStoreEntry.thirdCategoryEntries.length > 0 ? matchedStoreEntry.thirdCategoryEntries.map((entry) => entry.categoryName).filter(Boolean) : matchedStoreEntry?.thirdCategories || []; if (thirdCategoryOptions.length > 0) { return thirdCategoryOptions; } if (matchedStoreEntry && matchedStoreEntry.thirdCategories.length > 0) { return matchedStoreEntry.thirdCategories; } return params.config?.inputOptions?.thirdCategories || defaultAgentWorkflowUiConfig.inputOptions?.thirdCategories || []; } export function getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory(params: { config?: AgentWorkflowUiConfig | null; brand?: string | null; store?: string | null; thirdCategory?: string | null; }) { const entries = getAgentWorkflowBrandStoreEntries(params.config); const normalizedBrand = String(params.brand || "").trim().toLowerCase(); const normalizedStore = String(params.store || "").trim().toLowerCase(); const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase(); if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory) { return []; } const matchedEntry = entries.find( (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand ); const matchedStoreEntry = matchedEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore ); const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find( (thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory ); if (matchedThirdEntry && matchedThirdEntry.fourthCategories.length > 0) { return matchedThirdEntry.fourthCategories; } return params.config?.inputOptions?.fourthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fourthCategories || []; } export function getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth(params: { config?: AgentWorkflowUiConfig | null; brand?: string | null; store?: string | null; thirdCategory?: string | null; fourthCategory?: string | null; }) { const entries = getAgentWorkflowBrandStoreEntries(params.config); const normalizedBrand = String(params.brand || "").trim().toLowerCase(); const normalizedStore = String(params.store || "").trim().toLowerCase(); const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase(); const normalizedFourthCategory = String(params.fourthCategory || "").trim().toLowerCase(); if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory || !normalizedFourthCategory) { return []; } const matchedEntry = entries.find( (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand ); const matchedStoreEntry = matchedEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore ); const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find( (thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory ); const matchedFourthEntry = matchedThirdEntry?.fourthCategoryEntries?.find( (fourthEntry) => fourthEntry.categoryName.trim().toLowerCase() === normalizedFourthCategory ); if (matchedFourthEntry && matchedFourthEntry.fifthCategories.length > 0) { return matchedFourthEntry.fifthCategories; } return params.config?.inputOptions?.fifthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fifthCategories || []; } 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 && option.enabled !== false ); if (!matchedOption) break; selectedOptions.push(matchedOption); currentOptions = matchedOption.children || []; } return selectedOptions; } export function sanitizeAgentWorkflowSelections(params: { config?: AgentWorkflowUiConfig | null; selections: Partial>; }) { const config = params.config || defaultAgentWorkflowUiConfig; const enabledSteps = flattenAgentWorkflowSteps( getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []) ).map(({ step }) => step); const sanitized: Partial> = {}; 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; allowCrossLevel?: boolean; optionDepthLookup?: Map; }) { const selectedOptions = getAgentWorkflowSelectedOptionPath(params); const optionDepthLookup = params.optionDepthLookup || new Map( flattenAgentWorkflowOptionTree(params.step.options || []).map(({ option, depth }) => [ option.key, depth, ] as const) ); return Array.from( new Set( selectedOptions.flatMap((option) => { const currentDepth = optionDepthLookup.get(option.key); return (option.blockedOptionKeys || []).filter((blockedKey) => { if (!blockedKey) { return false; } if (params.allowCrossLevel) { return true; } if (typeof currentDepth !== "number") { return false; } return optionDepthLookup.get(blockedKey) === currentDepth; }); }) ) ); } function collectAgentWorkflowOptionBranchKeys( options: AgentWorkflowOptionUiConfig[], targetKey: string ): string[] { for (const option of options) { if (option.key === targetKey) { return flattenAgentWorkflowOptionTree([option]).map(({ option: currentOption }) => currentOption.key); } const childKeys = collectAgentWorkflowOptionBranchKeys(option.children || [], targetKey); if (childKeys.length > 0) { return childKeys; } } return []; } export function getAgentWorkflowBlockedOptionKeysForSelections(params: { config?: AgentWorkflowUiConfig | null; selections: Partial>; }) { const config = params.config || defaultAgentWorkflowUiConfig; const constraintSettings = getAgentWorkflowConstraintSettings(config); const visibleSteps = flattenVisibleAgentWorkflowSteps( getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []), params.selections ).map(({ step }) => step); const visibleOptionDepthLookup = new Map( visibleSteps.flatMap((step) => flattenAgentWorkflowOptionTree(step.options || []).map(({ option, depth }) => [ option.key, depth, ] as const) ) ); const directlyBlockedKeys = Array.from( new Set( visibleSteps.flatMap((step) => getAgentWorkflowBlockedOptionKeys({ step, selectionValue: params.selections[step.key], allowCrossLevel: constraintSettings.crossLevelMutualExclusionEnabled, optionDepthLookup: visibleOptionDepthLookup, }) ) ) ); return Array.from( new Set( directlyBlockedKeys.flatMap((blockedKey) => { const branchKeys = collectAgentWorkflowOptionBranchKeys( visibleSteps.flatMap((step) => step.options || []), blockedKey ); return branchKeys.length > 0 ? branchKeys : [blockedKey]; }) ) ); } 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; }