/** * 画个标 · AI 智能设计器 * 核心:把品牌需求 + 内置资源库(图标/布局/配色/风格/字体)作为白名单交给 DeepSeek, * AI 输出设计决策 JSON,系统校验后映射为完整方案(复用 generator 渲染) */ const HGB_AIDesigner = (function () { 'use strict'; /* ---------- 内置库解析 ---------- */ /** 从已加载的素材源按 "source:name" 解析图标对象 {id,name,svg,type}(懒加载适配:仅搜索已加载源,未加载返回 null) */ function resolveIcon(ref) { if (!ref || typeof ref !== 'string') return null; const hit = HGB_IconLib.findIcon(ref); return hit ? { id: hit.id, name: hit.name, svg: hit.svg, type: hit.type } : null; } /** 预加载 AI 输出引用的素材源(懒加载适配:AI 图标 ref 带源前缀,先加载对应源文件再同步查找) */ async function preloadReferencedSources(data) { const refs = (data.plans || []).map(p => p.icon).filter(Boolean); const sids = [...new Set(refs.map(r => String(r).split(':')[0]))].filter(sid => typeof HGB_BuiltinIcons !== 'undefined' && !!(HGB_BuiltinIcons.sources || {})[sid]); if (sids.length) await HGB_IconLib.preload(sids); } /** AI 可选的精选图标(带中文名,杜绝 AI 乱猜英文 id) */ const ICON_DEMO = [ ['lucide:star', '星星'], ['lucide:rocket', '火箭'], ['lucide:globe', '地球'], ['lucide:home', '房子'], ['lucide:heart', '爱心'], ['lucide:book', '书本'], ['lucide:graduation-cap', '学士帽'], ['lucide:briefcase', '公文包'], ['lucide:building-2', '大楼'], ['lucide:shield', '盾牌'], ['lucide:crown', '皇冠'], ['lucide:diamond', '钻石'], ['lucide:target', '靶心'], ['lucide:sun', '太阳'], ['lucide:moon', '月亮'], ['lucide:flame', '火焰'], ['lucide:leaf', '叶子'], ['lucide:tree-pine', '松树'], ['lucide:cloud', '云朵'], ['lucide:droplet', '水滴'], ['lucide:zap', '闪电'], ['lucide:sparkles', '星光'], ['lucide:medal', '奖牌'], ['lucide:trophy', '奖杯'], ['lucide:award', '勋章'], ['lucide:plane', '飞机'], ['lucide:compass', '指南针'], ['lucide:map', '地图'], ['lucide:music', '音符'], ['lucide:coffee', '咖啡'], ['lucide:utensils', '餐具'], ['lucide:shopping-bag', '购物袋'], ['lucide:palette', '调色板'], ['lucide:pen-tool', '画笔'], ['lucide:laptop', '笔记本'], ['lucide:phone', '手机'], ['lucide:camera', '相机'], ['lucide:key', '钥匙'], ['lucide:lock', '锁'], ['lucide:eye', '眼睛'], ['lucide:bell', '铃铛'], ['lucide:gift', '礼物'], ['lucide:party-popper', '礼花'], ['lucide:hand-heart', '手心爱心'], ['tabler:building', '楼宇'], ['tabler:chart-bar', '柱状图'], ['tabler:currency-yuan', '货币'], ['tabler:school', '学校'], ['tabler:heartbeat', '心跳'], ['feather:activity', '活动波'] ]; /** 规范化布局决策:非法修正 + 多样性(≥3 种布局、圆形环绕上限头像 4 / LOGO 2);头像模式布局池限定头像白名单 */ function normalizeLayoutDecisions(decisions, brand, avatar) { // 图形标识(Mark)模板不再内嵌品牌名,任何品牌名长度都可用; // 仅"环形环绕 Lockup"需要短品牌名 + 圆形 Mark const ROUND = ['badge', 'seal', 'orb', 'duo', 'burst']; const roundMax = avatar ? 4 : 2; // 头像模式允许更多圆形构图(差异化) // 布局池:头像模式 = 头像白名单;LOGO 模式 = 全部布局排除头像专有 const pool = avatar ? HGB_AvatarGen.AVATAR_LAYOUTS : HGB_Generator.LAYOUTS.filter(l => !l.avatarOnly).map(l => l.id); // 1) 非法布局 → 默认(圆标容器 / 头像白名单首个) for (const d of decisions) { if (!pool.includes(d.layout)) { d._layoutInvalid = true; d.layout = avatar ? pool[0] : HGB_Generator.defaultLayout(brand).id; } } // 2) 圆形 Mark 数 > 上限 → 多余的替换为其他布局(保证形状多样性) const roundIdx = decisions.map((d, i) => ROUND.includes(d.layout) ? i : -1).filter(i => i >= 0); if (roundIdx.length > roundMax) { const others = pool.filter(id => !ROUND.includes(id)); for (const i of roundIdx.slice(roundMax)) { decisions[i]._layoutInvalid = true; decisions[i].layout = others[i % others.length]; } } // 3) 布局种类 < 3 → 用未用过的布局补齐 const uniq = new Set(decisions.map(d => d.layout)); if (uniq.size < 3 && decisions.length >= 3) { const used = [...uniq]; const avail = pool.filter(id => !used.includes(id)); for (const d of decisions) { if (!avail.length) break; if (d.layout === used[0] && uniq.size < 3) { d._layoutInvalid = true; d.layout = avail.shift(); uniq.add(d.layout); } } } } /* ---------- Prompt 组装 ---------- */ function buildPrompt(opt) { const ind = opt.industry; const avatar = !!opt.avatar; // 头像模式:纯图形、无文字、布局限定头像白名单 const brand = (opt.brand || '').trim(); const sub = (opt.sub || '').trim(); const desc = (opt.desc || '').trim(); const userIcons = (opt.icons || []).map(i => i.id).join('、') || '(未指定)'; // 本次启用的技能(用户勾选):把技能标识信息注入,AI 须按技能规范设计 const skillList = (opt.skills || []).map(id => { const s = HGB_Skills.get(id); return s ? `${s.emoji} ${s.name}(${s.scene})` : null; }).filter(Boolean).join('\n') || '(未启用特定技能,按行业最佳实践设计)'; // 布局/风格/字体白名单(头像模式:布局只列头像白名单纯图形布局;LOGO 模式排除头像专有布局) const layoutPool = avatar ? HGB_Generator.LAYOUTS.filter(l => HGB_AvatarGen.isAvatarLayout(l.id)) : HGB_Generator.LAYOUTS.filter(l => !l.avatarOnly); const layoutList = layoutPool.map(l => `${l.id}:${l.name}(${l.desc})`).join('\n'); const lockupList = HGB_Generator.LOCKUP_POS.map(lp => `${lp.pos}:${lp.name}`).join('\n'); // 风格白名单:用户指定风格方向时仅列出所选风格 const userStyles = (opt.styleIds || []).filter(id => HGB_Styles.some(s => s.id === id)); const styleList = HGB_Styles.filter(s => !userStyles.length || userStyles.includes(s.id)) .map(s => `${s.id}:${s.name}(设计运动:${s.movement}|${s.desc};气质:${s.moods.join('/')})`).join('\n'); const stylePref = userStyles.length ? `用户指定了风格方向:${HGB_Styles.filter(s => userStyles.includes(s.id)).map(s => `${s.emoji}${s.name}`).join('、')}。styleId 必须从这些风格中选择;多套方案尽量覆盖不同的风格,展示该方向下的多种可能。` : '(用户未限定风格方向,请在全部风格中挑选契合行业与品牌描述的风格,并让多套方案使用不同的风格方向)'; const fontList = (ind.fontFamilies || []).join('、') + ';通用中文字体:SimHei、Microsoft YaHei、SimSun、KaiTi;通用英文字体:Arial、Georgia、Impact、Verdana、Montserrat、Playfair Display'; // 图标白名单:精选(带中文名)+ 当前行业图标 + 用户已选图标 const iconWhitelist = [ ...ICON_DEMO.map(([id, zh]) => `${id}(${zh})`), ...(ind.icons || []).map(i => `${ind.id}:${i.id}(行业图标)`), ...(opt.icons || []).map(i => `${i.id}(用户倾向图标)`) ].join('\n'); const sys = avatar ? `你是顶级的品牌头像设计师。请基于用户的品牌信息,从"内置设计资源库"中选择素材,设计多套专业头像(Avatar)方案。 架构理念(必须严格遵守): - 头像 = 纯图形标识(图标 + 容器/环/盾/几何),不含任何文字。头像用于微信/QQ/抖音/App 图标/网站 Favicon 等场景,小尺寸(64px)也必须清晰可辨。 设计原则(必须严格遵守): 1. 深刻理解品牌行业与描述,所选图标语义必须与品牌强相关(如餐饮用火焰/餐具,教育用书本/学士帽)。 2. 如果品牌描述指定色系/风格/气质,必须优先选择契合的配色与风格;如果用户明确指定了风格方向,必须只从指定风格中选择,不得越界。 3. 图标、布局、风格必须从各自白名单中选择,不得编造白名单之外的 id。 4. 每个方案给出贴切的中文方案名与设计理念(concept)。 5. 图标差异纪律:多套方案尽量使用不同的图标(可从白名单挑选语义相近但形态不同的图标),绝对避免所有方案共用同一个图标。 6. 布局纪律:${opt.count || 5} 套方案必须覆盖至少 3 种不同布局;头像圆形构图(badge/seal/orb/duo/burst)最多 4 套,优先使用头像专有布局(fullbleed 全出血大图标 / concentric 同心圆环 / centroid 中心单环)。 7. 构图方法论纪律:每套方案的图形标识必须建立在明确的几何构图框架上(容器/环/弧/角标/几何叠加),构图框架尽可能互不相同,禁止只靠"换颜色/换图标"制造差异。 8. 风格气韵纪律:所选 styleId 的设计运动(movement)与色系必须贯穿整套方案——主色/背景必须贴合风格气质(如 Art Deco 金黑对称、孟菲斯撞色拼贴、Swiss 黑白留白、霓虹深底荧光、包豪斯高饱和原色),禁止"风格与配色割裂"(如极简风配高饱和渐变)。系统会自动按 styleId 的色系渲染配色,无需在 JSON 中填写 paletteName。 9. 小尺寸可读性:图形轮廓简洁、对比清晰,避免过于纤细/密集的细节,保证缩到 64px(Favicon)仍可辨识。 只输出 JSON,不要输出任何其他文字。JSON 结构: { "overallConcept": "整体设计理念(一句话概括系列设计思路)", "plans": [ { "name": "方案一的中文名", "concept": "这套方案的设计理念说明", "icon": "lucide:graduation-cap", "layout": "orb", "styleId": "minimal" } ] } 其中 plans 数组包含 ${opt.count || 5} 个方案。icon 必须来自下方图标白名单;layout/styleId 分别来自对应白名单(配色由系统自动跟随 styleId 的风格色系,无需填写 paletteName)。` : `你是顶级的品牌 LOGO 设计师。请基于用户的品牌信息,从"内置设计资源库"中选择素材,设计多套专业 LOGO 方案。 架构理念(必须严格遵守): - LOGO 分两层:图形标识(Mark)= 纯图形(图标+容器+环),不含任何文字;品牌组合(Lockup)= Mark + 品牌名,品牌名只摆放在图形周边(下方/右侧/上方/环绕),绝不融进图形内部。 设计原则(必须严格遵守): 1. 深刻理解品牌行业与描述,所选图标语义必须与品牌强相关(如餐饮用火焰/餐具,教育用书本/学士帽)。 2. 如果品牌描述指定色系/风格/气质,必须优先选择契合的配色与风格;如果用户明确指定了风格方向,必须只从指定风格中选择,不得越界。 3. 图标、布局、配色、风格、字体、lockup 必须从各自白名单中选择,不得编造白名单之外的 id。 4. 每个方案给出贴切的中文方案名与设计理念(concept)。 5. 图标差异纪律:多套方案之间尽量使用不同的图标(可从白名单挑选语义相近但形态不同的图标),绝对避免所有方案共用同一个图标。 6. 布局纪律:${opt.count || 5} 套方案必须覆盖至少 3 种不同 Mark 布局;圆形 Mark(badge/seal/orb/duo/burst)最多 2 套。 7. lockup 纪律:品牌名摆放位置多样化,多套方案使用不同的 lockup 位置(如 2 套下方、1 套右侧、1 套上方);环形环绕(wrap)仅用于圆形 Mark 且品牌名 ≤6 中文字或 ≤12 西文字符。 8. 构图方法论纪律:每套方案的 Mark 必须建立在明确的几何构图框架上(容器/环/弧/绶带/角标/几何叠加),构图框架尽可能互不相同——借鉴"精确几何构图":圆与三角的数学比例、对称平衡、网格对齐;禁止只靠"换颜色/换图标/换位置"来制造差异。 9. 差异度纪律:多套方案必须在构图方式、视觉层次(环/盾/容器/绶带/角标/射线/拱门/几何叠加等)、配色、风格上有明显差异,呈现不同设计方向,而不是"换个颜色换个位置"。 10. 换色纪律:同一布局若复用,必须变换配色与风格;尽可能让每套方案的构图框架(layout)不同。 11. 风格气韵纪律:所选 styleId 的设计运动(movement)与色系必须贯穿整套方案——配色的主色/背景必须贴合风格气质(如 Art Deco 金黑对称、孟菲斯撞色拼贴、Swiss 黑白留白网格、霓虹深底荧光、包豪斯高饱和原色),禁止出现"风格与配色割裂"(如极简风配高饱和渐变)。系统会自动按 styleId 的色系渲染配色,无需在 JSON 中填写 paletteName。 只输出 JSON,不要输出任何其他文字。JSON 结构: { "overallConcept": "整体设计理念(一句话概括系列设计思路)", "plans": [ { "name": "方案一的中文名", "concept": "这套方案的设计理念说明", "icon": "lucide:graduation-cap", "layout": "orb", "lockup": "below", "styleId": "minimal", "fontCn": "SimHei", "fontEn": "Georgia" } ] } 其中 plans 数组包含 ${opt.count || 5} 个方案。icon 必须来自下方图标白名单;layout/styleId 分别来自对应白名单(配色由系统自动跟随 styleId 的风格色系,无需填写 paletteName);lockup 来自品牌组合位置白名单;fontCn/fontEn 来自字体白名单。`; const user = avatar ? `【品牌信息】 - 行业:${ind.name}(${ind.desc}) - 品牌名:${brand} - 品牌描述:${desc || '(无,请根据行业特点设计)'} - 用户倾向使用的图标:${userIcons} 【本次启用技能】(设计时须遵循这些技能的规范) ${skillList} 【风格方向要求】 ${stylePref} 【图标白名单】(icon 字段从这里选,格式为 源:名称) ${iconWhitelist} 【布局模板白名单】(layout 字段,仅限适合头像的纯图形布局,不含文字) ${layoutList} 【设计风格白名单】(styleId 字段;配色自动跟随所选风格的色系) ${styleList} 【字体白名单】(仅供方案理念参考;头像为纯图形,不渲染任何文字) ${fontList} 请设计 ${opt.count || 5} 套头像方案,每套都要从白名单选择,并附上设计理念。` : `【品牌信息】 - 行业:${ind.name}(${ind.desc}) - 品牌名:${brand} - 副标题:${sub || '(无)'} - 品牌描述:${desc || '(无,请根据行业特点设计)'} - 用户倾向使用的图标:${userIcons} 【本次启用技能】(设计时须遵循这些技能的规范) ${skillList} 【风格方向要求】 ${stylePref} 【图标白名单】(icon 字段从这里选,格式为 源:名称) ${iconWhitelist} 【布局模板白名单】(layout 字段) ${layoutList} 【品牌组合位置白名单】(lockup 字段,品牌名在图形周边的摆放方式) ${lockupList} 【设计风格白名单】(styleId 字段;配色自动跟随所选风格的色系) ${styleList} 【字体白名单】(fontCn / fontEn 字段) ${fontList} 请设计 ${opt.count || 5} 套方案,每套都要从白名单选择,并附上设计理念。`; return [ { role: 'system', content: sys }, { role: 'user', content: user } ]; } /* ---------- 解析与校验 ---------- */ function extractJson(content) { let text = (content || '').trim(); // 剥 markdown 围栏 text = text.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, ''); const start = text.indexOf('{'); const end = text.lastIndexOf('}'); if (start < 0 || end < start) throw new Error('AI 输出不是有效 JSON'); return JSON.parse(text.slice(start, end + 1)); } /** 校验并规范化 AI 输出:非法字段显式标记,布局统一规范 */ function validate(data, opt) { const ind = opt.industry; if (!data || !Array.isArray(data.plans) || !data.plans.length) { throw new Error('AI 未返回方案'); } const plans = data.plans.slice(0, 20); for (const p of plans) { // 图标:白名单解析,失败标记(映射时回退行业/用户图标) if (p.icon && !resolveIcon(p.icon)) p._iconInvalid = true; // 布局/风格:不在白名单标记(配色自动跟随风格,无需校验 paletteName) const layoutOk = opt.avatar ? HGB_AvatarGen.isAvatarLayout(p.layout) : HGB_Generator.LAYOUTS.some(l => l.id === p.layout && !l.avatarOnly); if (!layoutOk) p._layoutInvalid = true; if (!HGB_Styles.some(s => s.id === p.styleId)) p._styleInvalid = true; } // 布局纪律:非法修正 + 多样性保障(≥3 种布局、圆环类 ≤2) normalizeLayoutDecisions(plans, (opt.brand || '').trim() || HGB_Generator.sampleName(ind), !!opt.avatar); return data; } /** 颜色亮度(0-1) */ function lum(hex) { const c = String(hex || '#FFFFFF').replace('#', ''); if (c.length !== 6) return 0.9; const r = parseInt(c.slice(0, 2), 16) / 255; const g = parseInt(c.slice(2, 4), 16) / 255; const b = parseInt(c.slice(4, 6), 16) / 255; return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /** 对比度修正:深色主色 + 深色背景 → 背景自动调亮 */ function fixContrast(plan) { const text = plan.elements.find(el => el.type === 'text'); const icon = plan.elements.find(el => el.type === 'icon' && el.iconId !== 'ring' && el.iconId !== 'deco-bar'); const main = (text && text.fill) || (icon && icon.stroke); if (!main || !plan.bg) return; if (lum(plan.bg) < 0.42 && lum(main) < 0.45) { plan.bg = '#FFFFFF'; plan._bgFixed = true; } } /** 把 AI 决策映射为完整方案(非法字段显式修正并记录警告) */ function mapToPlan(p, opt, idx) { const ind = opt.industry; const avatar = !!opt.avatar; const brand = (opt.brand || '').trim() || HGB_Generator.sampleName(ind); const sub = (opt.sub || '').trim(); const userIcons = (opt.icons || []).filter(Boolean); const aiIcon = resolveIcon(p.icon); // 图标容错:AI 图标非法时,按方案序号轮换用户/行业图标,避免"全套同一图标" const fallbackPool = userIcons.length ? userIcons : ((ind.icons || [])); const icons = aiIcon ? [aiIcon] : (fallbackPool.length ? [fallbackPool[idx % fallbackPool.length]] : []); // 显式修正非法字段(不做随机回退);头像模式布局限定头像白名单 const layoutOk = avatar ? HGB_AvatarGen.isAvatarLayout(p.layout) : HGB_Generator.LAYOUTS.some(l => l.id === p.layout && !l.avatarOnly); const layoutId = layoutOk ? p.layout : (avatar ? HGB_AvatarGen.AVATAR_LAYOUTS[0] : HGB_Generator.defaultLayout(brand).id); // 风格:非法时优先回退用户指定风格池,其次行业推荐 const userStyleIds = (opt.styleIds || []).filter(id => HGB_Styles.some(s => s.id === id)); const styleId = HGB_Styles.some(s => s.id === p.styleId) ? p.styleId : (userStyleIds.length ? userStyleIds[idx % userStyleIds.length] : ((ind.styleIds || [])[0] || HGB_Styles[0].id)); const variant = HGB_Generator.generateVariant({ industry: ind, brand, sub, icons, layoutId, styleId, // 配色由 generator 自动跟随 styleId 色系 lockup: avatar ? '' : p.lockup, // 头像为纯图形,无品牌名组合 // AI 字体决策生效(非法字体由 fontFor 自动回退) fontFavs: [p.fontCn, p.fontEn].filter(Boolean) }); variant.id = 'ai_plan_' + Date.now().toString(36) + idx.toString(36); variant.name = (p.name || '').trim() || variant.name; variant.concept = (p.concept || '').trim(); variant.aiIcon = aiIcon ? aiIcon.name : ''; variant.ai = true; // 对比度自动修正 fixContrast(variant); // 收集自动修正警告,供卡片展示 const warnings = []; if (p._layoutInvalid) warnings.push('布局已自动调整'); if (p._iconInvalid) warnings.push('图标已替换'); if (p._styleInvalid) warnings.push('风格已调整'); if (variant._bgFixed) warnings.push('背景已调亮'); variant.warnings = warnings; return variant; } /* ---------- 主入口 ---------- */ /** * AI 生成方案 * @param {object} opt { industry, brand, sub, desc, icons, count } * @returns {Promise<{plans:Array, overallConcept:string}>} */ async function generatePlansAI(opt) { const messages = buildPrompt(opt); const content = await HGB_AI.chat(messages, { json: true, maxTokens: 8192 }); let data = extractJson(content); // 懒加载适配:预加载 AI 引用素材源,确保 resolveIcon/findIcon 命中 await preloadReferencedSources(data); data = validate(data, opt); const plans = data.plans.map((p, i) => mapToPlan(p, opt, i)); return { plans, overallConcept: (data.overallConcept || '').trim(), raw: content }; } return { generatePlansAI, resolveIcon, extractJson, validate, mapToPlan, buildPrompt }; })();