3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 21b74f52cd Address code review: extract magic number as named constant
Co-authored-by: liyupi <26037703+liyupi@users.noreply.github.com>
2026-02-22 06:43:41 +00:00
copilot-swe-agent[bot] 32aa591732 Add repository stats visualization: analysis script and generated HTML page
Co-authored-by: liyupi <26037703+liyupi@users.noreply.github.com>
2026-02-22 06:42:09 +00:00
copilot-swe-agent[bot] fd6e2b2e15 Initial plan 2026-02-22 06:33:52 +00:00
3 changed files with 453 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
const fs = require("fs");
const path = require("path");
// Directories to skip during analysis
const SKIP_DIRS = new Set([".git", ".github", ".vuepress", "node_modules", "translations"]);
/**
* Recursively collect markdown file info
* @param {string} dirPath
* @param {string} relativeTo - base path for computing relative paths
* @returns {Array<{relativePath: string, size: number, lines: number, chars: number, title: string, category: string}>}
*/
function collectMarkdownFiles(dirPath, relativeTo) {
const results = [];
let items;
try {
items = fs.readdirSync(dirPath, { withFileTypes: true });
} catch {
return results;
}
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
if (item.isDirectory()) {
if (SKIP_DIRS.has(item.name) || item.name.startsWith(".")) continue;
results.push(...collectMarkdownFiles(fullPath, relativeTo));
} else if (item.isFile() && item.name.endsWith(".md")) {
try {
const content = fs.readFileSync(fullPath, "utf-8");
const rel = path.relative(relativeTo, fullPath);
const parts = rel.split(path.sep);
const category = parts.length > 1 ? parts[0] : "根目录";
// Extract title from first heading or filename
const headingMatch = content.match(/^#\s+(.+)$/m);
const title = headingMatch ? headingMatch[1].trim() : item.name.replace(".md", "");
// Count Chinese characters + English words
const chineseChars = (content.match(/[\u4e00-\u9fff]/g) || []).length;
const englishWords = (content.match(/[a-zA-Z]+/g) || []).length;
results.push({
relativePath: rel,
size: Buffer.byteLength(content, "utf-8"),
lines: content.split("\n").length,
chars: content.length,
chineseChars,
englishWords,
title,
category,
subcategory: parts.length > 2 ? parts[1] : "",
});
} catch {
// skip unreadable files
}
}
}
return results;
}
/**
* Aggregate stats from file list
*/
function computeStats(files) {
const totalFiles = files.length;
const totalChars = files.reduce((s, f) => s + f.chars, 0);
const totalLines = files.reduce((s, f) => s + f.lines, 0);
const totalSize = files.reduce((s, f) => s + f.size, 0);
const totalChineseChars = files.reduce((s, f) => s + f.chineseChars, 0);
const totalEnglishWords = files.reduce((s, f) => s + f.englishWords, 0);
// Category breakdown
const categoryMap = {};
for (const f of files) {
if (!categoryMap[f.category]) {
categoryMap[f.category] = { fileCount: 0, chars: 0, lines: 0, size: 0, chineseChars: 0 };
}
categoryMap[f.category].fileCount++;
categoryMap[f.category].chars += f.chars;
categoryMap[f.category].lines += f.lines;
categoryMap[f.category].size += f.size;
categoryMap[f.category].chineseChars += f.chineseChars;
}
// Subcategory breakdown (for main categories)
const subcategoryMap = {};
for (const f of files) {
if (!f.subcategory) continue;
const key = f.category + " / " + f.subcategory;
if (!subcategoryMap[key]) {
subcategoryMap[key] = { fileCount: 0, chars: 0, chineseChars: 0 };
}
subcategoryMap[key].fileCount++;
subcategoryMap[key].chars += f.chars;
subcategoryMap[key].chineseChars += f.chineseChars;
}
// Top 15 longest files
const topFiles = [...files]
.sort((a, b) => b.chars - a.chars)
.slice(0, 15)
.map((f) => ({ title: f.title, chars: f.chars, chineseChars: f.chineseChars, category: f.category }));
// File size distribution
const sizeRanges = [
{ label: "< 1KB", min: 0, max: 1024 },
{ label: "1-5KB", min: 1024, max: 5120 },
{ label: "5-10KB", min: 5120, max: 10240 },
{ label: "10-50KB", min: 10240, max: 51200 },
{ label: "> 50KB", min: 51200, max: Infinity },
];
const sizeDistribution = sizeRanges.map((r) => ({
label: r.label,
count: files.filter((f) => f.size >= r.min && f.size < r.max).length,
}));
return {
totalFiles,
totalChars,
totalLines,
totalSize,
totalChineseChars,
totalEnglishWords,
categoryMap,
subcategoryMap,
topFiles,
sizeDistribution,
};
}
/**
* Generate the HTML stats page
*/
function generateHTML(stats) {
const categories = Object.entries(stats.categoryMap).sort((a, b) => b[1].fileCount - a[1].fileCount);
const subcategories = Object.entries(stats.subcategoryMap).sort((a, b) => b[1].fileCount - a[1].fileCount).slice(0, 20);
const categoryLabels = JSON.stringify(categories.map(([k]) => k));
const categoryFileCounts = JSON.stringify(categories.map(([, v]) => v.fileCount));
const categoryChinese = JSON.stringify(categories.map(([, v]) => v.chineseChars));
const sizeLabels = JSON.stringify(stats.sizeDistribution.map((d) => d.label));
const sizeCounts = JSON.stringify(stats.sizeDistribution.map((d) => d.count));
const MAX_CHART_TITLE_LENGTH = 18;
const topLabels = JSON.stringify(stats.topFiles.map((f) => f.title.length > MAX_CHART_TITLE_LENGTH ? f.title.slice(0, MAX_CHART_TITLE_LENGTH) + "…" : f.title));
const topChars = JSON.stringify(stats.topFiles.map((f) => f.chineseChars));
const subLabels = JSON.stringify(subcategories.map(([k]) => k));
const subCounts = JSON.stringify(subcategories.map(([, v]) => v.fileCount));
const formatNum = (n) => n.toLocaleString("zh-CN");
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鱼皮 AI 知识库 - 数据统计</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f0f2f5; color: #333; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 40px 20px; text-align: center; }
.header h1 { font-size: 2em; margin-bottom: 8px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin: 24px 0; }
.stat-card { background: #fff; border-radius: 12px; padding: 24px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-4px); box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
.stat-card .number { font-size: 2em; font-weight: 700; color: #667eea; }
.stat-card .label { font-size: 0.9em; color: #666; margin-top: 4px; }
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 20px; margin: 20px 0; }
.chart-box { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.chart-box h3 { margin-bottom: 16px; color: #444; font-size: 1.1em; }
.chart-box canvas { width: 100% !important; }
.table-box { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin: 20px 0; overflow-x: auto; }
.table-box h3 { margin-bottom: 16px; color: #444; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; color: #555; font-weight: 600; }
tr:hover { background: #f8f9fe; }
.footer { text-align: center; padding: 24px; color: #999; font-size: 0.85em; }
@media (max-width: 600px) {
.charts { grid-template-columns: 1fr; }
.header h1 { font-size: 1.5em; }
.stat-card .number { font-size: 1.5em; }
}
</style>
</head>
<body>
<div class="header">
<h1>📊 鱼皮 AI 知识库 · 数据统计</h1>
<p>自动分析仓库内容,可视化展示教程统计数据</p>
</div>
<div class="container">
<div class="summary">
<div class="stat-card"><div class="number">${formatNum(stats.totalFiles)}</div><div class="label">📄 文档总数</div></div>
<div class="stat-card"><div class="number">${formatNum(stats.totalChineseChars)}</div><div class="label">✍️ 中文字数</div></div>
<div class="stat-card"><div class="number">${formatNum(stats.totalEnglishWords)}</div><div class="label">🔤 英文单词</div></div>
<div class="stat-card"><div class="number">${formatNum(stats.totalLines)}</div><div class="label">📝 总行数</div></div>
<div class="stat-card"><div class="number">${(stats.totalSize / 1024).toFixed(0)} KB</div><div class="label">💾 内容体积</div></div>
<div class="stat-card"><div class="number">${categories.length}</div><div class="label">📂 内容分类</div></div>
</div>
<div class="charts">
<div class="chart-box">
<h3>📁 各分类文档数量</h3>
<canvas id="categoryFileChart"></canvas>
</div>
<div class="chart-box">
<h3>✍️ 各分类中文字数</h3>
<canvas id="categoryCharsChart"></canvas>
</div>
<div class="chart-box">
<h3>📏 文件大小分布</h3>
<canvas id="sizeChart"></canvas>
</div>
<div class="chart-box">
<h3>📂 子分类文档数量 (Top 20)</h3>
<canvas id="subCategoryChart"></canvas>
</div>
</div>
<div class="table-box">
<h3>🏆 内容最丰富的文档 (Top 15)</h3>
<table>
<thead><tr><th>#</th><th>标题</th><th>分类</th><th>中文字数</th></tr></thead>
<tbody>
${stats.topFiles.map((f, i) => `<tr><td>${i + 1}</td><td>${f.title}</td><td>${f.category}</td><td>${formatNum(f.chineseChars)}</td></tr>`).join("\n ")}
</tbody>
</table>
</div>
<div class="table-box">
<h3>📊 各分类详细数据</h3>
<table>
<thead><tr><th>分类</th><th>文档数</th><th>中文字数</th><th>总行数</th><th>体积</th></tr></thead>
<tbody>
${categories.map(([k, v]) => `<tr><td>${k}</td><td>${v.fileCount}</td><td>${formatNum(v.chineseChars)}</td><td>${formatNum(v.lines)}</td><td>${(v.size / 1024).toFixed(1)} KB</td></tr>`).join("\n ")}
</tbody>
</table>
</div>
</div>
<div class="footer">
<p>数据自动生成于 ${new Date().toLocaleDateString("zh-CN")} · <a href="https://github.com/liyupi/ai-guide" target="_blank">GitHub</a></p>
</div>
<script>
const COLORS = ['#667eea','#764ba2','#f093fb','#f5576c','#4facfe','#00f2fe','#43e97b','#fa709a','#fee140','#30cfd0','#a18cd1','#fbc2eb','#ff9a9e','#fad0c4','#ffecd2'];
new Chart(document.getElementById('categoryFileChart'), {
type: 'pie',
data: {
labels: ${categoryLabels},
datasets: [{ data: ${categoryFileCounts}, backgroundColor: COLORS }]
},
options: { responsive: true, plugins: { legend: { position: 'right' } } }
});
new Chart(document.getElementById('categoryCharsChart'), {
type: 'bar',
data: {
labels: ${categoryLabels},
datasets: [{ label: '中文字数', data: ${categoryChinese}, backgroundColor: '#667eea' }]
},
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } } }
});
new Chart(document.getElementById('sizeChart'), {
type: 'doughnut',
data: {
labels: ${sizeLabels},
datasets: [{ data: ${sizeCounts}, backgroundColor: COLORS.slice(0, 5) }]
},
options: { responsive: true, plugins: { legend: { position: 'right' } } }
});
new Chart(document.getElementById('subCategoryChart'), {
type: 'bar',
data: {
labels: ${subLabels},
datasets: [{ label: '文档数', data: ${subCounts}, backgroundColor: '#764ba2' }]
},
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } } }
});
</script>
</body>
</html>`;
}
// Main
const rootDir = path.resolve(__dirname, "../..");
const files = collectMarkdownFiles(rootDir, rootDir);
const stats = computeStats(files);
const html = generateHTML(stats);
const outputPath = path.resolve(rootDir, "stats.html");
fs.writeFileSync(outputPath, html, "utf-8");
console.log(`统计页面已生成: ${outputPath}`);
console.log(`共分析 ${stats.totalFiles} 个文档,${stats.totalChineseChars.toLocaleString()} 个中文字,${stats.totalLines.toLocaleString()}`);
+1
View File
@@ -5,6 +5,7 @@
"generate:sidebar": "node ./.vuepress/scripts/generateSidebar.js",
"generate:readme": "node ./.vuepress/scripts/genReadme.js",
"getMdNumber": "node ./.vuepress/scripts/getMdNumber.js",
"generate:stats": "node ./.vuepress/scripts/generateStats.js",
"docs:dev": "vuepress dev .",
"pre-docs:build": "npm run generate:sidebar ./AI && npm run generate:readme ./AI",
"docs:build": "vuepress build .",
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鱼皮 AI 知识库 - 数据统计</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f0f2f5; color: #333; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 40px 20px; text-align: center; }
.header h1 { font-size: 2em; margin-bottom: 8px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin: 24px 0; }
.stat-card { background: #fff; border-radius: 12px; padding: 24px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-4px); box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
.stat-card .number { font-size: 2em; font-weight: 700; color: #667eea; }
.stat-card .label { font-size: 0.9em; color: #666; margin-top: 4px; }
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 20px; margin: 20px 0; }
.chart-box { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.chart-box h3 { margin-bottom: 16px; color: #444; font-size: 1.1em; }
.chart-box canvas { width: 100% !important; }
.table-box { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin: 20px 0; overflow-x: auto; }
.table-box h3 { margin-bottom: 16px; color: #444; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; color: #555; font-weight: 600; }
tr:hover { background: #f8f9fe; }
.footer { text-align: center; padding: 24px; color: #999; font-size: 0.85em; }
@media (max-width: 600px) {
.charts { grid-template-columns: 1fr; }
.header h1 { font-size: 1.5em; }
.stat-card .number { font-size: 1.5em; }
}
</style>
</head>
<body>
<div class="header">
<h1>📊 鱼皮 AI 知识库 · 数据统计</h1>
<p>自动分析仓库内容,可视化展示教程统计数据</p>
</div>
<div class="container">
<div class="summary">
<div class="stat-card"><div class="number">297</div><div class="label">📄 文档总数</div></div>
<div class="stat-card"><div class="number">664,156</div><div class="label">✍️ 中文字数</div></div>
<div class="stat-card"><div class="number">87,184</div><div class="label">🔤 英文单词</div></div>
<div class="stat-card"><div class="number">57,693</div><div class="label">📝 总行数</div></div>
<div class="stat-card"><div class="number">2894 KB</div><div class="label">💾 内容体积</div></div>
<div class="stat-card"><div class="number">4</div><div class="label">📂 内容分类</div></div>
</div>
<div class="charts">
<div class="chart-box">
<h3>📁 各分类文档数量</h3>
<canvas id="categoryFileChart"></canvas>
</div>
<div class="chart-box">
<h3>✍️ 各分类中文字数</h3>
<canvas id="categoryCharsChart"></canvas>
</div>
<div class="chart-box">
<h3>📏 文件大小分布</h3>
<canvas id="sizeChart"></canvas>
</div>
<div class="chart-box">
<h3>📂 子分类文档数量 (Top 20)</h3>
<canvas id="subCategoryChart"></canvas>
</div>
</div>
<div class="table-box">
<h3>🏆 内容最丰富的文档 (Top 15)</h3>
<table>
<thead><tr><th>#</th><th>标题</th><th>分类</th><th>中文字数</th></tr></thead>
<tbody>
<tr><td>1</td><td>Cursor + LangChain4j - AI 编程助手项目实战</td><td>Vibe Coding 零基础教程</td><td>7,532</td></tr>
<tr><td>2</td><td>DeepSeek技术解读:从V3到R1的MoE架构创新</td><td>AI</td><td>15,198</td></tr>
<tr><td>3</td><td>优质 AI 编程扩展推荐</td><td>Vibe Coding 零基础教程</td><td>9,768</td></tr>
<tr><td>4</td><td>2025 年 11 月 AI 资讯汇总</td><td>AI</td><td>8,958</td></tr>
<tr><td>5</td><td>DeepSeek最强专业拆解:清交复教授超硬核解读</td><td>AI</td><td>15,700</td></tr>
<tr><td>6</td><td>Vibe Coding 概念大全</td><td>Vibe Coding 零基础教程</td><td>10,013</td></tr>
<tr><td>7</td><td>炸裂!Spring AI 1.0 正式发布,让 Java 再次伟大!</td><td>AI</td><td>3,048</td></tr>
<tr><td>8</td><td>Model Context Protocol,看这一篇就够了</td><td>AI</td><td>4,120</td></tr>
<tr><td>9</td><td>AI</td><td>AI</td><td>6,534</td></tr>
<tr><td>10</td><td>Vibe Coding 网站美化技巧</td><td>Vibe Coding 零基础教程</td><td>4,557</td></tr>
<tr><td>11</td><td>GLM-5 + OpenClaw:打造你的 AI 伴侣</td><td>Vibe Coding 零基础教程</td><td>6,529</td></tr>
<tr><td>12</td><td>Vibe Coding 效率提升技巧</td><td>Vibe Coding 零基础教程</td><td>5,277</td></tr>
<tr><td>13</td><td>Agent Skills:通用 AI 技能库</td><td>Vibe Coding 零基础教程</td><td>4,165</td></tr>
<tr><td>14</td><td>AI 辅助工具集</td><td>Vibe Coding 零基础教程</td><td>5,334</td></tr>
<tr><td>15</td><td>Vibe Coding 项目灵感大全</td><td>Vibe Coding 零基础教程</td><td>9,683</td></tr>
</tbody>
</table>
</div>
<div class="table-box">
<h3>📊 各分类详细数据</h3>
<table>
<thead><tr><th>分类</th><th>文档数</th><th>中文字数</th><th>总行数</th><th>体积</th></tr></thead>
<tbody>
<tr><td>AI</td><td>190</td><td>305,919</td><td>19,930</td><td>1342.2 KB</td></tr>
<tr><td>Vibe Coding 零基础教程</td><td>91</td><td>329,547</td><td>35,130</td><td>1423.7 KB</td></tr>
<tr><td>产品服务</td><td>14</td><td>26,046</td><td>2,274</td><td>110.3 KB</td></tr>
<tr><td>根目录</td><td>2</td><td>2,644</td><td>359</td><td>17.6 KB</td></tr>
</tbody>
</table>
</div>
</div>
<div class="footer">
<p>数据自动生成于 2026/2/22 · <a href="https://github.com/liyupi/ai-guide" target="_blank">GitHub</a></p>
</div>
<script>
const COLORS = ['#667eea','#764ba2','#f093fb','#f5576c','#4facfe','#00f2fe','#43e97b','#fa709a','#fee140','#30cfd0','#a18cd1','#fbc2eb','#ff9a9e','#fad0c4','#ffecd2'];
new Chart(document.getElementById('categoryFileChart'), {
type: 'pie',
data: {
labels: ["AI","Vibe Coding 零基础教程","产品服务","根目录"],
datasets: [{ data: [190,91,14,2], backgroundColor: COLORS }]
},
options: { responsive: true, plugins: { legend: { position: 'right' } } }
});
new Chart(document.getElementById('categoryCharsChart'), {
type: 'bar',
data: {
labels: ["AI","Vibe Coding 零基础教程","产品服务","根目录"],
datasets: [{ label: '中文字数', data: [305919,329547,26046,2644], backgroundColor: '#667eea' }]
},
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } } }
});
new Chart(document.getElementById('sizeChart'), {
type: 'doughnut',
data: {
labels: ["< 1KB","1-5KB","5-10KB","10-50KB","> 50KB"],
datasets: [{ data: [8,93,94,98,4], backgroundColor: COLORS.slice(0, 5) }]
},
options: { responsive: true, plugins: { legend: { position: 'right' } } }
});
new Chart(document.getElementById('subCategoryChart'), {
type: 'bar',
data: {
labels: ["AI / AI行业资讯","AI / AI应用场景","Vibe Coding 零基础教程 / 20 项目实战","Vibe Coding 零基础教程 / 10 编程工具","AI / DeepSeek使用指南","Vibe Coding 零基础教程 / 50 产品变现","Vibe Coding 零基础教程 / 40 编程学习","Vibe Coding 零基础教程 / 30 经验技巧","AI / DeepSeek技术解析","AI / AI项目教程","产品服务 / 产品","AI / DeepSeek资源汇总","AI / 鱼皮的 AI 指南","AI / 关于DeepSeek","产品服务 / 编程学习"],
datasets: [{ label: '文档数', data: [106,34,22,20,17,16,14,13,12,6,6,5,5,4,2], backgroundColor: '#764ba2' }]
},
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } } }
});
</script>
</body>
</html>