#!/usr/bin/env node /** * 生成 Bookworm 技能触发关键词一览表 (HTML) * 数据来源: skills-index.json + route-stats.json + route-weights.json */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const index = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills-index.json'), 'utf8')); const stats = JSON.parse(fs.readFileSync(path.join(ROOT, 'debug', 'route-stats.json'), 'utf8')); const weights = JSON.parse(fs.readFileSync(path.join(ROOT, 'debug', 'route-weights.json'), 'utf8')); const activeHits = stats.stats || {}; const deltas = weights.deltas || {}; const skills = (index.skills || []).sort((a, b) => { const hA = activeHits[a.name] || 0; const hB = activeHits[b.name] || 0; if (hB !== hA) return hB - hA; return a.name.localeCompare(b.name); }); const totalKw = skills.reduce((a, s) => a + (s.keywords || []).length, 0); const activeCount = Object.keys(activeHits).length; const dormantCount = skills.length - activeCount; const composableCount = skills.filter(s => s.isComposable).length; const importedCount = skills.filter(s => s.maturity === 'imported').length; const deltaCount = Object.keys(deltas).length; const penalizedCount = skills.filter(s => s.coldPenalty).length; const aliasCount = skills.reduce((a, s) => a + (s.keywords || []).filter(k => k.tier === 'alias').length, 0); const now = new Date().toISOString(); const dateStr = now.slice(0, 10); // === 生成 HTML === const lines = []; function w(s) { lines.push(s); } w(''); w(''); w(''); w(''); w(''); w('Bookworm v5.9 — Skills 技能触发关键词一览表'); w(''); w(''); w(''); // Header w('
'); w('

Bookworm v5.9 — Skills 技能触发关键词一览表

'); w('
共 ' + skills.length + ' 个技能 · ' + totalKw + ' 个关键词 · 生成于 ' + dateStr + '
'); w('
'); // Stats bar w('
'); w('
' + skills.length + '
总技能
'); w('
' + activeCount + '
活跃技能
'); w('
' + dormantCount + '
休眠技能
'); w('
' + composableCount + '
可组合
'); w('
' + totalKw + '
关键词
'); w('
' + aliasCount + '
同义词
'); w('
' + penalizedCount + '
降权技能
'); w('
' + deltaCount + '
学习调整
'); w('
'); // Legend w('
'); w('
core 核心词 (1.0)
'); w('
strong 强关联 (0.8)
'); w('
extended 扩展词 (0.5)
'); w('
alias 同义词 (0.5)
'); w('
有学习 delta
'); w('
coldPenalty 降权
'); w('
'); // Filter bar w('
'); w(' '); w(' '); w(' '); w(' '); w(' '); w(' '); w('
'); // Search w(''); // Grid w('
'); const tierOrder = { core: 0, primary: 1, secondary: 2, tertiary: 3, alias: 4 }; skills.forEach(skill => { const hits = activeHits[skill.name] || 0; const isActive = hits > 0; const skillDeltas = deltas[skill.name] || {}; const kws = skill.keywords || []; const sortedKws = [...kws].sort((a, b) => { const tA = tierOrder[a.tier] !== undefined ? tierOrder[a.tier] : 5; const tB = tierOrder[b.tier] !== undefined ? tierOrder[b.tier] : 5; if (tA !== tB) return tA - tB; return b.weight - a.weight; }); const hasPenalty = !!skill.coldPenalty; let cls = isActive ? 'card' : 'card dormant'; if (hasPenalty) cls += ' penalized'; const kwStr = kws.map(k => k.keyword).join(',').toLowerCase().replace(/"/g, '"'); const desc = (skill.description || '').replace(//g, '>').replace(/"/g, '"'); w('
'); w('
'); w('
' + skill.name + '/' + skill.name + '
'); w('
'); if (isActive) { w(' 活跃'); w(' ' + hits + ' 次'); } else { w(' 休眠'); } if (hasPenalty) w(' penalty ' + skill.coldPenalty + ''); if (skill.isComposable) w(' composable'); if (skill.maturity === 'imported') w(' imported'); w('
'); w('
'); w('
' + desc + '
'); w('
'); sortedKws.forEach(kw => { const tier = kw.tier || 'tertiary'; const delta = skillDeltas[kw.keyword.toLowerCase()] || 0; const deltaCls = delta !== 0 ? ' delta' : ''; const deltaHtml = delta !== 0 ? '' + (delta > 0 ? '+' : '') + delta + '' : ''; const title = 'tier: ' + tier + ' | weight: ' + kw.weight.toFixed(2) + (delta ? ' | delta: ' + delta : ''); w(' ' + kw.keyword + '' + kw.weight.toFixed(1) + '' + deltaHtml + ''); }); w('
'); w('
'); }); w('
'); // Footer w(''); // Script w(''); w(''); w(''); // 写入文件 const outDir = path.join(ROOT, 'docs'); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); const outPath = path.join(outDir, 'skills-keywords-overview.html'); fs.writeFileSync(outPath, lines.join('\n'), 'utf8'); const size = (fs.statSync(outPath).size / 1024).toFixed(1); console.log('HTML 已生成: ' + outPath); console.log('文件大小: ' + size + ' KB'); console.log('技能数: ' + skills.length + ' | 关键词: ' + totalKw + ' | 活跃: ' + activeCount + ' | 休眠: ' + dormantCount);