388 lines
21 KiB
JavaScript
388 lines
21 KiB
JavaScript
/**
|
||
* 画个标 · 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 };
|
||
})();
|