/** * 冒烟:头像模式 AI 路径(aiDesigner buildPrompt/validate/mapToPlan) * 验证: * 1) buildPrompt 头像版布局白名单 = 头像布局、不含 lockup 白名单 * 2) validate + normalizeLayoutDecisions 把 AI 布局修正到头像白名单内 * 3) mapToPlan 产物 layoutId 在头像白名单、lockup 为空、无文字元素 */ 'use strict'; const fs = require('fs'); const path = require('path'); const vm = require('vm'); const ROOT = path.resolve(__dirname, '..'); // 加载顺序:数据 → 核心(依赖全局常量 HGB_*) const files = [ 'js/data/icons.js', 'js/data/styles.js', 'js/data/industries.js', 'js/data/builtin-icons.js', 'js/data/decorations.js', 'js/core/generator.js', 'js/core/avatarGen.js', 'js/skills/skills.js', 'js/core/ai.js', 'js/core/aiDesigner.js' ]; for (const f of files) { const src = fs.readFileSync(path.join(ROOT, f), 'utf8'); // vm 在全局作用域执行,使 HGB_* 成为全局词法绑定 vm.runInThisContext(src, { filename: f }); } const ind = HGB_Industries.find('food'); const opt = { industry: ind, brand: '川味小馆', sub: '', desc: '川菜馆,麻辣鲜香', icons: [], styleIds: [], skills: ['composition', 'contrast-check'], count: 5, avatar: true }; let pass = 0, fail = 0; function check(name, ok, detail) { if (ok) { pass++; console.log(' ✅ ' + name + (detail ? ' — ' + detail : '')); } else { fail++; console.log(' ❌ ' + name + (detail ? ' — ' + detail : '')); } } // 1) Prompt 头像版 const msgs = HGB_AIDesigner.buildPrompt(opt); const sys = msgs[0].content, user = msgs[1].content; const avatarLayoutIds = HGB_AvatarGen.AVATAR_LAYOUTS; const fullLayoutIds = HGB_Generator.LAYOUTS.map(l => l.id); console.log('== 1) buildPrompt 头像版 =='); check('系统提示为头像设计师', /头像设计师/.test(sys)); // 布局白名单:LOGO 独有的布局(monogram 等)不应出现在头像 prompt const logoOnly = fullLayoutIds.filter(id => !avatarLayoutIds.includes(id)); check('头像 prompt 不含 LOGO 专属布局', !logoOnly.some(id => new RegExp('\\b' + id + '\\b').test(user)), '排除: ' + logoOnly.join('/')); check('头像 prompt 含头像布局', avatarLayoutIds.every(id => new RegExp('\\b' + id + '\\b').test(user)) || user.includes('orb')); check('头像 prompt 不含 lockup 白名单', !user.includes('lockup') && !user.includes('品牌组合位置')); check('头像 prompt 强调无文字/纯图形', /纯图形|不含任何文字/.test(sys)); // 2) validate:给一批非法布局(含 LOGO 专属 monogram/ribbon),验证全部修正到头像白名单 console.log('== 2) validate 布局修正 =='); const fake = { overallConcept: 'x', plans: [ { name: 'A', concept: 'c', icon: 'bad', layout: 'monogram', styleId: 'minimal' }, { name: 'B', concept: 'c', icon: 'bad', layout: 'ribbon', styleId: 'artdeco' }, { name: 'C', concept: 'c', icon: 'bad', layout: 'badge', styleId: 'neon' }, { name: 'D', concept: 'c', icon: 'bad', layout: 'seal', styleId: 'minimal' }, { name: 'E', concept: 'c', icon: 'bad', layout: 'orb', styleId: 'minimal' } ] }; const data = HGB_AIDesigner.validate(fake, opt); const layouts = data.plans.map(p => p.layout); const uniq = new Set(layouts); check('全部布局在头像白名单', layouts.every(l => avatarLayoutIds.includes(l)), layouts.join('/')); check('非法布局被修正并标记', layouts.includes('orb') || data.plans.some(p => p._layoutInvalid)); check('布局多样性 ≥3', uniq.size >= 3, '种类 ' + uniq.size); const round = layouts.filter(l => ['badge', 'seal', 'orb', 'duo', 'burst'].includes(l)).length; check('头像圆形构图 ≤4', round <= 4, '圆形 ' + round); // 3) mapToPlan 产物结构(icon 非法 → 回退行业图标) console.log('== 3) mapToPlan 产物 =='); const plans = data.plans.map((p, i) => HGB_AIDesigner.mapToPlan(p, opt, i)); for (let i = 0; i < plans.length; i++) { const p = plans[i]; check('方案' + (i + 1) + ' layoutId 在头像白名单', avatarLayoutIds.includes(p.layoutId), p.layoutId); const hasText = (p.markElements || []).some(el => el.type === 'text'); check('方案' + (i + 1) + ' Mark 无文字元素', !hasText); check('方案' + (i + 1) + ' 有 ai 标记与概念', !!p.ai && !!p.concept); check('方案' + (i + 1) + ' 配色跟随风格', !!p.paletteName && !!p.bg); } // 图标差异:AI 图标非法时按序号轮换行业图标,不应全套同一 const iconSet = new Set(plans.map(p => (p.aiIcon || '') + '|' + (p.markElements || []).filter(e => e.type === 'icon').map(e => e.iconId).join(','))); check('图标有差异(>1 种)', iconSet.size > 1, '种类 ' + iconSet.size); // 4) 头像专有布局(avatarOnly)差异化:LOGO 全局池排除、头像白名单包含 console.log('== 4) 头像专有布局差异化 =='); const avatarOnlyIds = HGB_Generator.LAYOUTS.filter(l => l.avatarOnly).map(l => l.id); check('存在 3 个头像专有布局', avatarOnlyIds.length === 3 && JSON.stringify(avatarOnlyIds) === JSON.stringify(['fullbleed', 'concentric', 'centroid']), avatarOnlyIds.join('/')); // LOGO 模式 generatePlans(无白名单)不得出现头像专有布局 const logoPlans = HGB_Generator.generatePlans({ industry: ind, brand: '川味小馆', count: 12 }); check('LOGO 模式 12 套全部布局合法', logoPlans.every(p => !avatarOnlyIds.includes(p.layoutId))); check('LOGO 模式布局多样', new Set(logoPlans.map(p => p.layoutId)).size >= 5); // 头像模式 generateAvatars 白名单包含头像专有布局 check('头像白名单含头像专有布局', avatarOnlyIds.every(id => HGB_AvatarGen.AVATAR_LAYOUTS.includes(id))); const avatarPlans = HGB_AvatarGen.generateAvatars({ industry: ind, brand: '川味小馆', count: 20 }); const avLayouts = new Set(avatarPlans.map(p => p.layoutId)); check('头像 20 套产出头像专有布局', avatarOnlyIds.some(id => avLayouts.has(id)), '头像布局 ' + avLayouts.size + ' 种: ' + [...avLayouts].slice(0, 8).join('/') + '…'); check('头像 20 套全部在白名单内', avatarPlans.every(p => HGB_AvatarGen.isAvatarLayout(p.layoutId))); // AI 校验:头像 validate 允许头像专有布局;LOGO validate 标记头像专有布局非法 const avFake = { overallConcept: 'x', plans: [ { name: 'A', concept: 'c', icon: 'bad', layout: 'fullbleed', styleId: 'minimal' }, { name: 'B', concept: 'c', icon: 'bad', layout: 'concentric', styleId: 'neon' }, { name: 'C', concept: 'c', icon: 'bad', layout: 'centroid', styleId: 'minimal' }, { name: 'D', concept: 'c', icon: 'bad', layout: 'orb', styleId: 'minimal' }, { name: 'E', concept: 'c', icon: 'bad', layout: 'seal', styleId: 'artdeco' } ]}; const avData = HGB_AIDesigner.validate(avFake, opt); check('AI 头像接受头像专有布局', avData.plans.every(p => avatarOnlyIds.includes(p.layout) || HGB_AvatarGen.isAvatarLayout(p.layout))); const logoOpt = { ...opt, avatar: false }; const lgFake = { overallConcept: 'x', plans: [ { name: 'A', concept: 'c', icon: 'bad', layout: 'fullbleed', styleId: 'minimal' }, { name: 'B', concept: 'c', icon: 'bad', layout: 'badge', styleId: 'minimal' }, { name: 'C', concept: 'c', icon: 'bad', layout: 'seal', styleId: 'minimal' }, { name: 'D', concept: 'c', icon: 'bad', layout: 'orb', styleId: 'minimal' }, { name: 'E', concept: 'c', icon: 'bad', layout: 'monogram', styleId: 'minimal' } ]}; const lgData = HGB_AIDesigner.validate(lgFake, logoOpt); const lgLayouts = lgData.plans.map(p => p.layout); check('AI LOGO 排除头像专有布局', lgLayouts.every(l => !avatarOnlyIds.includes(l)), lgLayouts.join('/')); console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败'); process.exit(fail ? 1 : 0);