/** * 画个标 · 核心:表情设计师 AI 编排(Emoji AI) * AI 输出结构化设计决策 JSON(布局/风格/动画/分层)→ 系统负责渲染与导出; * AI 失败/无 Key 时返回 null,由 emojiGen 降级为内置随机池(页面不空白)。 * * 设计要点: * - 与 aiDesigner 同模式:buildPrompt → HGB_AI.chat(json) → extractJson → validate → 决策 * - validate 把所有输出修正到合法白名单(布局/风格/动画类型/参数),非法即回退 * - 纯函数 + 环境分级容错(Node 无头无 HGB_AI 时返回 null,不算失败) */ const HGB_EmojiAI = (function () { 'use strict'; /* ============ Prompt 编排 ============ */ function layoutList() { if (typeof HGB_EmojiGen !== 'undefined') return HGB_EmojiGen.EMOJI_LAYOUTS.join('/'); return 'orb/badge/seal/burst/duo/emblem/hexagon/diamond/arch/geo/techcorner/fullbleed/concentric/centroid'; } function buildPrompt(opt) { const ind = opt.industry || {}; const iconDesc = (opt.icons && opt.icons.length) ? opt.icons.map(i => i.name || i.id).join('、') : '(未指定,由 AI 建议行业意象)'; const mood = opt.mood ? '用户期望情绪:' + opt.mood + ';' : ''; // 情绪-动画映射(生成前置,先审后生成:用户给出期望情绪时优先匹配,避免动效与情绪割裂) const moodMap = opt.mood ? '情绪-动画映射参考(用户给出期望情绪时优先匹配,可适度发挥但勿选明显冲突类型):' + '开心→bounce、生气→shake、惊讶→heartbeat、难过→float、兴奋→hopspin、平静→breath。' : ''; const sys = [ '你是「画个标」的动态表情设计师。用户给出行业与图标素材,你输出「一张静态底图 + 一个主动画」的结构化设计决策。', '铁律:只输出 JSON,不要任何解释文字。', '字段与取值:', '- overallConcept:一句话设计概念(中文,10-20 字)', '- base.layoutId:从白名单选一个:' + layoutList(), '- base.styleId:从系统风格库选一个(如 minimal/neon/flat/artdeco/corporate 等,按行业气质匹配)', '- anim.type:从动画库选一个:bounce/pulse/wiggle/float/spin/fade/twirl/heartbeat/shake/breath/hopspin', '- anim.speed:0.6-1.5(周期数,生气/兴奋偏快,平静偏慢)', '- anim.amplitude:0.7-1.4(强度,活泼可大,克制宜小)', '- anim.layered:true/false(是否元素分层错峰动画,让表情更活;多元素图形建议 true)', '- anim.concept:动效概念一句话(中文)', moodMap, '示例:{"overallConcept":"咖啡的松弛感","base":{"layoutId":"seal","styleId":"neon"},"anim":{"type":"float","speed":0.8,"amplitude":0.9,"layered":true,"concept":"蒸汽缓缓漂浮"}}' ].join('\n'); const user = [ '行业:' + (ind.name || '通用') + '(' + (ind.emoji || '') + ',' + (ind.desc || '') + ')', '品牌/主题:' + (opt.brand || '动态表情'), '用户图标素材:' + iconDesc, mood, '请输出严格 JSON(不要 markdown 代码块)。' ].filter(Boolean).join('\n'); return [ { role: 'system', content: sys }, { role: 'user', content: user } ]; } /* ============ 解析与校验 ============ */ /** 从聊天文本提取 JSON 对象(取第一个完整 {} 块,容忍前后缀文字) */ function extractJson(text) { if (!text || typeof text !== 'string') return null; const start = text.indexOf('{'); if (start < 0) return null; let depth = 0, inStr = false, esc = false; for (let i = start; i < text.length; i++) { const c = text[i]; if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; } else if (c === '"') inStr = true; else if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) { try { return JSON.parse(text.slice(start, i + 1)); } catch (e) { return null; } } } } return null; } function inList(v, list) { return list.indexOf(v) >= 0; } /** 修正 AI 输出到合法白名单(非法项回退合理默认,返回完整决策对象) */ function validate(d, opt) { const ind = opt.industry || {}; const dec = d || {}; const base = dec.base || {}; const anim = dec.anim || {}; const layouts = (typeof HGB_EmojiGen !== 'undefined') ? HGB_EmojiGen.EMOJI_LAYOUTS : []; const animTypes = (typeof HGB_Motion !== 'undefined') ? HGB_Motion.animTypes() : []; const styleIds = (typeof HGB_Styles !== 'undefined') ? HGB_Styles.map(s => s.id) : []; let layoutId = inList(base.layoutId, layouts) ? base.layoutId : null; if (!layoutId && ind.favLayout && inList(ind.favLayout, layouts)) layoutId = ind.favLayout; if (!layoutId) layoutId = layouts[0] || 'orb'; const styleId = inList(base.styleId, styleIds) ? base.styleId : (opt.styleIds && opt.styleIds[0]); const animType = inList(anim.type, animTypes.map(t => t.id)) ? anim.type : null; let aType = animType || 'bounce'; const clamp = (v, lo, hi, dft) => { const n = Number(v); return (isFinite(n) && n >= lo && n <= hi) ? n : dft; }; return { overallConcept: String(dec.overallConcept || '').trim().slice(0, 30) || '动态表情包', base: { layoutId, styleId }, anim: { type: aType, speed: clamp(anim.speed, 0.6, 1.5, 1), amplitude: clamp(anim.amplitude, 0.7, 1.4, 1), layered: !!anim.layered, concept: String(anim.concept || '').trim().slice(0, 30) } }; } /* ============ 主入口 ============ */ /** * AI 表情设计决策(失败/无 Key/超时 → 返回 null,供生成器降级) * @param {object} opt { industry, brand, icons, mood } * @returns {Promise<{overallConcept, base:{layoutId,styleId}, anim:{...}}|null>} */ async function suggest(opt) { if (typeof HGB_AI === 'undefined') return null; try { if (typeof HGB_AI.hasKey === 'function' && !HGB_AI.hasKey()) return null; const messages = buildPrompt(opt); const content = await HGB_AI.chat(messages, { json: true, maxTokens: 2048 }); const d = extractJson(content); if (!d) return null; return validate(d, opt); } catch (e) { return null; // AI 失败 → 生成器降级内置池 } } return { buildPrompt, extractJson, validate, suggest }; })(); // Node 无头冒烟自检(浏览器同样执行,幂等) if (typeof module !== 'undefined' && module.exports) module.exports = HGB_EmojiAI;