feat: 调整页面顶部区域&重构项目页热力图&优化markdown内容页
This commit is contained in:
@@ -23,6 +23,19 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// best-effort:同步失败不阻断 build
|
||||
const syncProjectsExit = runNode(path.join(repoRoot, 'scripts', 'sync-projects.js'));
|
||||
if (syncProjectsExit !== 0)
|
||||
log.warn('sync-projects 异常退出,已继续(best-effort)', { exit: syncProjectsExit });
|
||||
|
||||
const syncHeatmapExit = runNode(path.join(repoRoot, 'scripts', 'sync-heatmap.js'));
|
||||
if (syncHeatmapExit !== 0)
|
||||
log.warn('sync-heatmap 异常退出,已继续(best-effort)', { exit: syncHeatmapExit });
|
||||
|
||||
const syncArticlesExit = runNode(path.join(repoRoot, 'scripts', 'sync-articles.js'));
|
||||
if (syncArticlesExit !== 0)
|
||||
log.warn('sync-articles 异常退出,已继续(best-effort)', { exit: syncArticlesExit });
|
||||
|
||||
const generatorExit = runNode(path.join(repoRoot, 'src', 'generator.js'));
|
||||
if (generatorExit !== 0) {
|
||||
log.error('generate 失败', { exit: generatorExit });
|
||||
|
||||
@@ -46,6 +46,10 @@ async function main() {
|
||||
if (syncProjectsExit !== 0)
|
||||
log.warn('sync-projects 异常退出,已继续(best-effort)', { exit: syncProjectsExit });
|
||||
|
||||
const syncHeatmapExit = runNode(path.join(repoRoot, 'scripts', 'sync-heatmap.js'));
|
||||
if (syncHeatmapExit !== 0)
|
||||
log.warn('sync-heatmap 异常退出,已继续(best-effort)', { exit: syncHeatmapExit });
|
||||
|
||||
const syncArticlesExit = runNode(path.join(repoRoot, 'scripts', 'sync-articles.js'));
|
||||
if (syncArticlesExit !== 0)
|
||||
log.warn('sync-articles 异常退出,已继续(best-effort)', { exit: syncArticlesExit });
|
||||
|
||||
184
scripts/sync-heatmap.js
Normal file
184
scripts/sync-heatmap.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/* eslint-disable no-console */
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { loadConfig } = require('../src/generator.js');
|
||||
const {
|
||||
extractYearlyContributionsInnerHtml,
|
||||
} = require('../src/generator/utils/githubContributions');
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('sync:heatmap');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
enabled: true,
|
||||
cacheDir: 'dev',
|
||||
fetch: {
|
||||
timeoutMs: 10_000,
|
||||
userAgent: 'MeNavHeatmapSync/1.0',
|
||||
},
|
||||
};
|
||||
|
||||
function parseBooleanEnv(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') return fallback;
|
||||
const v = String(value).trim().toLowerCase();
|
||||
if (v === '1' || v === 'true' || v === 'yes' || v === 'y') return true;
|
||||
if (v === '0' || v === 'false' || v === 'no' || v === 'n') return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseIntegerEnv(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') return fallback;
|
||||
const n = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function getSettings(config) {
|
||||
const fromConfig =
|
||||
config && config.site && config.site.github && typeof config.site.github === 'object'
|
||||
? config.site.github
|
||||
: {};
|
||||
|
||||
const merged = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...fromConfig,
|
||||
fetch: {
|
||||
...DEFAULT_SETTINGS.fetch,
|
||||
...(fromConfig.fetch || {}),
|
||||
},
|
||||
};
|
||||
|
||||
merged.enabled = parseBooleanEnv(process.env.HEATMAP_ENABLED, merged.enabled);
|
||||
|
||||
// 复用 projects 的 cacheDir 逻辑,保持所有 dev 缓存在同一目录
|
||||
merged.cacheDir = process.env.PROJECTS_CACHE_DIR
|
||||
? String(process.env.PROJECTS_CACHE_DIR)
|
||||
: process.env.HEATMAP_CACHE_DIR
|
||||
? String(process.env.HEATMAP_CACHE_DIR)
|
||||
: merged.cacheDir;
|
||||
|
||||
merged.fetch.timeoutMs = parseIntegerEnv(
|
||||
process.env.HEATMAP_FETCH_TIMEOUT,
|
||||
merged.fetch.timeoutMs
|
||||
);
|
||||
merged.fetch.timeoutMs = Math.max(1_000, merged.fetch.timeoutMs);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function findProjectsPages(config) {
|
||||
const pages = [];
|
||||
const nav = Array.isArray(config.navigation) ? config.navigation : [];
|
||||
nav.forEach((item) => {
|
||||
const pageId = item && item.id ? String(item.id) : '';
|
||||
if (!pageId || !config[pageId]) return;
|
||||
const page = config[pageId];
|
||||
const templateName = page && page.template ? String(page.template) : pageId;
|
||||
if (templateName !== 'projects') return;
|
||||
pages.push({ pageId, page });
|
||||
});
|
||||
return pages;
|
||||
}
|
||||
|
||||
function getGithubUsernameFromConfig(config) {
|
||||
const username =
|
||||
config && config.site && config.site.github && config.site.github.username
|
||||
? String(config.site.github.username).trim()
|
||||
: '';
|
||||
return username;
|
||||
}
|
||||
|
||||
async function fetchTextWithTimeout(url, { timeoutMs, headers }) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET', headers, signal: controller.signal });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return await response.text();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
const config = loadConfig();
|
||||
const settings = getSettings(config);
|
||||
|
||||
log.info('开始');
|
||||
|
||||
if (!settings.enabled) {
|
||||
log.ok('heatmap 同步已禁用,跳过', { env: 'HEATMAP_ENABLED=false' });
|
||||
return;
|
||||
}
|
||||
|
||||
const username = getGithubUsernameFromConfig(config);
|
||||
if (!username) {
|
||||
log.ok('未配置 site.github.username,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const pages = findProjectsPages(config);
|
||||
if (!pages.length) {
|
||||
log.ok('未找到 template=projects 的页面,跳过同步');
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheBaseDir = path.isAbsolute(settings.cacheDir)
|
||||
? settings.cacheDir
|
||||
: path.join(process.cwd(), settings.cacheDir);
|
||||
ensureDir(cacheBaseDir);
|
||||
|
||||
const url = `https://github.com/users/${encodeURIComponent(username)}/contributions`;
|
||||
const headers = {
|
||||
'user-agent': settings.fetch.userAgent,
|
||||
accept: 'text/html',
|
||||
};
|
||||
|
||||
let html;
|
||||
try {
|
||||
html = await fetchTextWithTimeout(url, { timeoutMs: settings.fetch.timeoutMs, headers });
|
||||
} catch (error) {
|
||||
log.warn('获取 GitHub contributions 失败(best-effort)', {
|
||||
url,
|
||||
message: String(error && error.message ? error.message : error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const innerHtml = extractYearlyContributionsInnerHtml(html);
|
||||
if (!innerHtml) {
|
||||
log.warn('解析 contributions HTML 失败(best-effort)', { url, username });
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { pageId } of pages) {
|
||||
const payload = {
|
||||
version: '1.0',
|
||||
pageId,
|
||||
generatedAt: new Date().toISOString(),
|
||||
username,
|
||||
sourceUrl: url,
|
||||
html: innerHtml,
|
||||
};
|
||||
|
||||
const cachePath = path.join(cacheBaseDir, `${pageId}.heatmap-cache.json`);
|
||||
fs.writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8');
|
||||
log.ok('写入 heatmap 缓存', { page: pageId, cache: cachePath });
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), pages: pages.length, username });
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
log.error('执行异常(best-effort,不阻断后续 build)', {
|
||||
message: error && error.message ? error.message : String(error),
|
||||
});
|
||||
if (isVerbose() && error && error.stack) console.error(error.stack);
|
||||
process.exitCode = 0; // best-effort:不阻断后续 build
|
||||
});
|
||||
Reference in New Issue
Block a user