#!/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('');
// Stats bar
w('');
w('
');
w('
');
w('
');
w('
' + composableCount + '
可组合
');
w('
');
w('
');
w('
' + penalizedCount + '
降权技能
');
w('
');
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('');
w(' ');
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);