import type { AgentWorkflowBrandStoreEntry, AgentWorkflowOptionUiConfig, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, } from "@/types/agent"; export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config"; export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3; export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 3; 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 || "", 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, 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>, 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; } 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))); } 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 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 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(); 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 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, 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); 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: normalizedSteps, }; } 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 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; }