// 临时脚本 v2:意象白名单 + 词边界匹配 + 黑名单过滤,重选候选池 const fs = require('fs'); const path = require('path'); const vm = require('vm'); const ROOT = process.cwd(); const OUT = '_design-test'; fs.mkdirSync(OUT, { recursive: true }); const manifest = JSON.parse(fs.readFileSync('js/data/manifest/icons.json', 'utf8')); const srcMap = {}; manifest.sources.forEach(s => { srcMap[s.id] = s; }); const TARGETS = ['feather', 'lucide', 'tabler', 'bootstrap', 'remix', 'heroicons', 'iconoir', 'phosphor']; // 意象白名单:品牌感强的核心词(拆词匹配,词必须独立出现) const THEMES = { design: ['pencil', 'pen', 'palette', 'brush', 'paint', 'edit', 'ruler', 'compass', 'pen-tool', 'highlighter', 'marker', 'swatch', 'frame', 'crop', 'layers', 'grid', 'layout', 'design', 'sketch', 'ink', 'draft'], brand: ['flag', 'tag', 'badge', 'bookmark', 'award', 'crown', 'star', 'sparkle', 'label', 'target', 'certificate', 'ribbon', 'medal', 'trophy'], symbol: ['hexagon', 'diamond', 'triangle', 'shield', 'anchor', 'gem', 'bolt', 'zap', 'infinity', 'sun', 'moon', 'leaf', 'flower', 'mountain', 'wave', 'snowflake', 'crosshair', 'asterisk'], tech: ['rocket', 'cpu', 'cloud', 'code', 'terminal', 'wifi', 'database', 'globe', 'chip', 'robot', 'server', 'bug'], commerce: ['bag', 'cart', 'coin', 'wallet', 'briefcase', 'chart', 'store', 'gift', 'receipt', 'percent'], misc: ['key', 'lock', 'mail', 'phone', 'calendar', 'clock', 'home', 'book', 'heart', 'camera', 'map', 'plane', 'music', 'mic', 'coffee', 'cup'] }; // 黑名单:语义负面/错误/禁用类,绝不做 LOGO const BLACK = ['off', 'alert', 'error', 'warning', 'forbidden', 'ban', 'question', 'slash', 'offline', 'disconnected', 'exclamation', 'trash', 'remove', 'delete', 'block', 'stop']; function words(name) { return (name || '').toLowerCase().split(/[-_ ]+/).filter(Boolean); } function loadSource(sid) { const file = path.join(ROOT, 'js/data/sources', sid + '.js'); if (!fs.existsSync(file)) return null; const code = fs.readFileSync(file, 'utf8'); const sb = {}; sb.window = sb; sb.self = sb; sb.globalThis = sb; vm.runInNewContext(code, sb, { filename: sid + '.js' }); const src = sb.window['HGB_Source_' + sid]; return src ? src.icons : null; } const out = []; const seen = new Set(); for (const sid of TARGETS) { const cfg = srcMap[sid]; if (!cfg) continue; const icons = loadSource(sid); if (!icons) { console.log('SKIP: ' + sid); continue; } for (const [theme, kws] of Object.entries(THEMES)) { for (const kw of kws) { const kwWords = words(kw); const hits = icons.filter(ic => { const ws = words(ic.name); if (!ws.length) return false; if (ws.some(w => BLACK.includes(w))) return false; // 白名词须完全包含(词集合 ⊇ 白名词集合) return kwWords.every(w => ws.includes(w)) && !seen.has(sid + ':' + ic.id); }); const pick = hits.slice(0, 1); // 每源每意象取 1 个,控制量 for (const h of pick) { seen.add(sid + ':' + h.id); out.push({ theme, source: sid, id: h.id, name: h.name, categoryCn: h.categoryCn || '', type: h.type || cfg.type, svg: h.svg, license: cfg.license, licenseNote: cfg.licenseNote, coordText: cfg.coordText, style: cfg.style || '' }); } } } console.log(` 加载 ${sid} → 命中 ${out.filter(o => o.source === sid).length}`); } fs.writeFileSync(path.join(OUT, 'candidates2.json'), JSON.stringify(out, null, 1), 'utf8'); console.log(`\n候选池 v2 共 ${out.length} 个 → candidates2.json`); const byTheme = {}; out.forEach(o => { byTheme[o.theme] = (byTheme[o.theme] || 0) + 1; }); console.log('主题分布:' + JSON.stringify(byTheme));