feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+387
View File
@@ -0,0 +1,387 @@
/**
* 画个标 · 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 };
})();