feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user