140 lines
7.5 KiB
JavaScript
140 lines
7.5 KiB
JavaScript
/**
|
||
* 冒烟:头像模式 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);
|