import type { AgentWorkflowOptionUiConfig, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, } from "@/types/agent"; export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config"; function cloneOption( option: AgentWorkflowOptionUiConfig ): AgentWorkflowOptionUiConfig { return { ...option, description: option.description || "", previewImageUrl: option.previewImageUrl || "", promptFragment: option.promptFragment || "", }; } function cloneStep(step: AgentWorkflowStepUiConfig): AgentWorkflowStepUiConfig { return { ...step, description: step.description || "", options: step.options.map(cloneOption), }; } function createStep( key: string, title: string, description: string, options: AgentWorkflowOptionUiConfig[] ): AgentWorkflowStepUiConfig { return { key, title, description, enabled: true, sortOrder: 0, options, }; } function createOption( key: string, buttonName: string, promptFragment: string, sortOrder: number ): AgentWorkflowOptionUiConfig { return { key, buttonName, description: "", previewImageUrl: "", enabled: true, sortOrder, promptFragment, }; } 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: [], }, steps: defaultSteps.map((step, index) => cloneStep({ ...step, sortOrder: index, }) ), }; 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 defaultStepByKey = new Map( defaultAgentWorkflowUiConfig.steps.map((step) => [step.key, step] as const) ); const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]); const usedStepKeys = new Set(); const normalizedSteps = sourceSteps .map((item, stepIndex) => { 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) => { const optionRecord = option && typeof option === "object" && !Array.isArray(option) ? (option as Record) : null; const defaultOption = defaultStep?.options.find( (item) => item.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; 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, }; }) .sort((left, right) => left.sortOrder - right.sortOrder); 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, }; }) .sort((left, right) => left.sortOrder - right.sortOrder); return { inputOptions: { brands: normalizeStringList(rawInputOptions.brands), stores: normalizeStringList(rawInputOptions.stores), }, steps: normalizedSteps.sort((left, right) => left.sortOrder - right.sortOrder), }; }