bookworm-smart-assistant/scripts/archive/generate-skills-html.js

239 lines
14 KiB
JavaScript
Raw Permalink Normal View History

#!/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('<!DOCTYPE html>');
w('<html lang="zh-CN">');
w('<head>');
w('<meta charset="UTF-8">');
w('<meta name="viewport" content="width=device-width, initial-scale=1.0">');
w('<title>Bookworm v5.9 — Skills 技能触发关键词一览表</title>');
w('<style>');
w(':root { --bg:#0d1117; --card:#161b22; --border:#30363d; --text:#c9d1d9; --muted:#8b949e; --accent:#58a6ff; --green:#3fb950; --yellow:#d29922; --red:#f85149; --orange:#db6d28; }');
w('* { margin:0; padding:0; box-sizing:border-box; }');
w('body { font-family:-apple-system,"Segoe UI",Helvetica,Arial,sans-serif; background:var(--bg); color:var(--text); padding:20px; }');
w('.header { text-align:center; padding:30px 0 20px; }');
w('.header h1 { font-size:28px; color:#fff; margin-bottom:8px; }');
w('.header .sub { color:var(--muted); font-size:14px; }');
w('.stats-bar { display:flex; justify-content:center; gap:30px; margin:20px 0 30px; flex-wrap:wrap; }');
w('.stat .num { font-size:32px; font-weight:700; color:var(--accent); text-align:center; }');
w('.stat .label { font-size:12px; color:var(--muted); text-transform:uppercase; text-align:center; }');
w('.legend { display:flex; justify-content:center; gap:16px; margin-bottom:20px; flex-wrap:wrap; }');
w('.legend-item { display:flex; align-items:center; gap:4px; font-size:12px; color:var(--muted); }');
w('.legend-dot { width:10px; height:10px; border-radius:2px; display:inline-block; }');
w('.filter-bar { display:flex; justify-content:center; gap:10px; margin-bottom:20px; flex-wrap:wrap; }');
w('.fbtn { padding:6px 14px; border:1px solid var(--border); background:var(--card); color:var(--muted); border-radius:16px; cursor:pointer; font-size:12px; transition:all .2s; }');
w('.fbtn:hover,.fbtn.active { border-color:var(--accent); color:var(--accent); background:rgba(88,166,255,.1); }');
w('.search-box { display:flex; justify-content:center; margin-bottom:24px; }');
w('.search-box input { width:400px; max-width:90vw; padding:10px 16px; background:var(--card); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:14px; outline:none; }');
w('.search-box input:focus { border-color:var(--accent); }');
w('.search-box input::placeholder { color:var(--muted); }');
w('.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(480px,1fr)); gap:16px; max-width:1400px; margin:0 auto; }');
w('.card { background:var(--card); border:1px solid var(--border); border-radius:10px; padding:16px; transition:border-color .2s; }');
w('.card:hover { border-color:var(--accent); }');
w('.card.dormant { opacity:.6; }');
w('.card.dormant:hover { opacity:1; }');
w('.card-hd { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }');
w('.sname { font-size:15px; font-weight:600; color:#fff; }');
w('.sname .cmd { color:var(--accent); font-size:12px; font-weight:400; margin-left:6px; }');
w('.badges { display:flex; gap:6px; }');
w('.b { padding:2px 8px; border-radius:10px; font-size:11px; font-weight:500; }');
w('.b-act { background:rgba(63,185,80,.15); color:var(--green); }');
w('.b-drm { background:rgba(139,148,158,.15); color:var(--muted); }');
w('.b-hit { background:rgba(88,166,255,.15); color:var(--accent); }');
w('.b-cmp { background:rgba(219,109,40,.15); color:var(--orange); }');
w('.b-imp { background:rgba(139,148,158,.15); color:var(--muted); border:1px dashed var(--muted); }');
w('.desc { font-size:12px; color:var(--muted); margin-bottom:10px; line-height:1.4; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }');
w('.kws { display:flex; flex-wrap:wrap; gap:4px; }');
w('.kw { padding:2px 8px; border-radius:4px; font-size:11px; cursor:default; }');
w('.kw[data-t="core"] { background:rgba(231,76,60,.15); color:#e74c3c; border:1px solid rgba(231,76,60,.3); }');
w('.kw[data-t="strong"] { background:rgba(230,126,34,.15); color:#e67e22; border:1px solid rgba(230,126,34,.3); }');
w('.kw[data-t="extended"] { background:rgba(241,196,15,.12); color:#f1c40f; border:1px solid rgba(241,196,15,.25); }');
w('.kw[data-t="tertiary"] { background:rgba(149,165,166,.1); color:#95a5a6; border:1px solid rgba(149,165,166,.2); }');
w('.kw[data-t="alias"] { background:rgba(52,152,219,.12); color:#3498db; border:1px solid rgba(52,152,219,.25); }');
w('.b-pen { background:rgba(248,81,73,.15); color:var(--red); }');
w('.card.penalized { border-left:3px solid var(--red); }');
w('.kw.delta { outline:2px solid var(--green); outline-offset:1px; }');
w('.kw .di { font-size:9px; vertical-align:super; margin-left:2px; color:var(--green); }');
w('.kw .wt { font-size:9px; color:var(--muted); margin-left:2px; }');
w('.footer { text-align:center; padding:30px 0; color:var(--muted); font-size:12px; }');
w('@media(max-width:600px) { .grid{grid-template-columns:1fr;} }');
w('</style>');
w('</head>');
w('<body>');
// Header
w('<div class="header">');
w(' <h1>Bookworm v5.9 — Skills 技能触发关键词一览表</h1>');
w(' <div class="sub">共 ' + skills.length + ' 个技能 · ' + totalKw + ' 个关键词 · 生成于 ' + dateStr + '</div>');
w('</div>');
// Stats bar
w('<div class="stats-bar">');
w(' <div class="stat"><div class="num">' + skills.length + '</div><div class="label">总技能</div></div>');
w(' <div class="stat"><div class="num">' + activeCount + '</div><div class="label">活跃技能</div></div>');
w(' <div class="stat"><div class="num">' + dormantCount + '</div><div class="label">休眠技能</div></div>');
w(' <div class="stat"><div class="num">' + composableCount + '</div><div class="label">可组合</div></div>');
w(' <div class="stat"><div class="num">' + totalKw + '</div><div class="label">关键词</div></div>');
w(' <div class="stat"><div class="num">' + aliasCount + '</div><div class="label">同义词</div></div>');
w(' <div class="stat"><div class="num">' + penalizedCount + '</div><div class="label">降权技能</div></div>');
w(' <div class="stat"><div class="num">' + deltaCount + '</div><div class="label">学习调整</div></div>');
w('</div>');
// Legend
w('<div class="legend">');
w(' <div class="legend-item"><span class="legend-dot" style="background:#e74c3c"></span> core 核心词 (1.0)</div>');
w(' <div class="legend-item"><span class="legend-dot" style="background:#e67e22"></span> strong 强关联 (0.8)</div>');
w(' <div class="legend-item"><span class="legend-dot" style="background:#f1c40f"></span> extended 扩展词 (0.5)</div>');
w(' <div class="legend-item"><span class="legend-dot" style="background:#3498db"></span> alias 同义词 (0.5)</div>');
w(' <div class="legend-item"><span class="legend-dot" style="background:#3fb950;outline:2px solid #3fb950;width:8px;height:8px"></span> 有学习 delta</div>');
w(' <div class="legend-item"><span class="legend-dot" style="background:var(--red)"></span> coldPenalty 降权</div>');
w('</div>');
// Filter bar
w('<div class="filter-bar">');
w(' <button class="fbtn active" onclick="filterSkills(\'all\',this)">全部 (' + skills.length + ')</button>');
w(' <button class="fbtn" onclick="filterSkills(\'active\',this)">活跃 (' + activeCount + ')</button>');
w(' <button class="fbtn" onclick="filterSkills(\'dormant\',this)">休眠 (' + dormantCount + ')</button>');
w(' <button class="fbtn" onclick="filterSkills(\'composable\',this)">可组合 (' + composableCount + ')</button>');
w(' <button class="fbtn" onclick="filterSkills(\'imported\',this)">导入 (' + importedCount + ')</button>');
w(' <button class="fbtn" onclick="filterSkills(\'penalized\',this)">降权 (' + penalizedCount + ')</button>');
w('</div>');
// Search
w('<div class="search-box">');
w(' <input type="text" id="searchInput" placeholder="搜索技能名或关键词..." oninput="searchSkills(this.value)">');
w('</div>');
// Grid
w('<div class="grid" id="skillGrid">');
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, '&quot;');
const desc = (skill.description || '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
w('<div class="' + cls + '" data-name="' + skill.name + '" data-active="' + isActive + '" data-composable="' + (skill.isComposable || false) + '" data-maturity="' + (skill.maturity || 'stable') + '" data-penalized="' + hasPenalty + '" data-keywords="' + kwStr + '">');
w(' <div class="card-hd">');
w(' <div class="sname">' + skill.name + '<span class="cmd">/' + skill.name + '</span></div>');
w(' <div class="badges">');
if (isActive) {
w(' <span class="b b-act">活跃</span>');
w(' <span class="b b-hit">' + hits + ' 次</span>');
} else {
w(' <span class="b b-drm">休眠</span>');
}
if (hasPenalty) w(' <span class="b b-pen">penalty ' + skill.coldPenalty + '</span>');
if (skill.isComposable) w(' <span class="b b-cmp">composable</span>');
if (skill.maturity === 'imported') w(' <span class="b b-imp">imported</span>');
w(' </div>');
w(' </div>');
w(' <div class="desc">' + desc + '</div>');
w(' <div class="kws">');
sortedKws.forEach(kw => {
const tier = kw.tier || 'tertiary';
const delta = skillDeltas[kw.keyword.toLowerCase()] || 0;
const deltaCls = delta !== 0 ? ' delta' : '';
const deltaHtml = delta !== 0 ? '<span class="di">' + (delta > 0 ? '+' : '') + delta + '</span>' : '';
const title = 'tier: ' + tier + ' | weight: ' + kw.weight.toFixed(2) + (delta ? ' | delta: ' + delta : '');
w(' <span class="kw' + deltaCls + '" data-t="' + tier + '" title="' + title + '">' + kw.keyword + '<span class="wt">' + kw.weight.toFixed(1) + '</span>' + deltaHtml + '</span>');
});
w(' </div>');
w('</div>');
});
w('</div>');
// Footer
w('<div class="footer">');
w(' Bookworm Smart Assistant v5.9 · 数据来源: skills-index.json + route-stats.json + route-weights.json<br>');
w(' 生成时间: ' + now + ' · Active Skills: ' + activeCount + '/' + skills.length);
w('</div>');
// Script
w('<script>');
w('function filterSkills(type, btn) {');
w(' document.querySelectorAll(".fbtn").forEach(b => b.classList.remove("active"));');
w(' if (btn) btn.classList.add("active");');
w(' document.querySelectorAll(".card").forEach(c => {');
w(' const show = type==="all" || (type==="active"&&c.dataset.active==="true") || (type==="dormant"&&c.dataset.active==="false") || (type==="composable"&&c.dataset.composable==="true") || (type==="imported"&&c.dataset.maturity==="imported") || (type==="penalized"&&c.dataset.penalized==="true");');
w(' c.style.display = show ? "" : "none";');
w(' });');
w('}');
w('function searchSkills(q) {');
w(' q = q.toLowerCase().trim();');
w(' document.querySelectorAll(".card").forEach(c => {');
w(' if (!q) { c.style.display=""; return; }');
w(' const match = (c.dataset.name||"").includes(q) || (c.dataset.keywords||"").includes(q);');
w(' c.style.display = match ? "" : "none";');
w(' });');
w(' if (q) document.querySelectorAll(".fbtn").forEach(b => b.classList.remove("active"));');
w(' else document.querySelector(".fbtn").classList.add("active");');
w('}');
w('</script>');
w('</body>');
w('</html>');
// 写入文件
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);