239 lines
14 KiB
JavaScript
239 lines
14 KiB
JavaScript
|
|
#!/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, '"');
|
||
|
|
const desc = (skill.description || '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
|
|
|
||
|
|
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);
|