const fs = require("node:fs");
const path = require("node:path");
const blogRoot = "d:\\Code\\Obsidian\\博客";
const coverDir = path.join(blogRoot, "covers");
fs.mkdirSync(coverDir, { recursive: true });
const existingCoverMap = new Map([
[
path.join(blogRoot, "AI与大模型", "深入解析 Claude Code:Vibe Coding 时代的 AI 编程利器.md"),
"claude-code-vibe-coding-cover.svg",
],
[path.join(blogRoot, "编程与工具", "Docker部署完全指南.md"), "docker-deployment-guide-cover.svg"],
[path.join(blogRoot, "机器学习", "视觉语言模型技术综述.md"), "vision-language-model-cover.svg"],
]);
function escapeXml(text) {
return String(text ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function walkMarkdownFiles(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "covers") continue;
files.push(...walkMarkdownFiles(fullPath));
continue;
}
if (entry.isFile() && entry.name.endsWith(".md")) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b, "zh-CN"));
}
function getArticleTitle(filePath) {
const raw = fs.readFileSync(filePath, "utf8");
const titleMatch = raw.match(/^title:\s*(.+)$/m);
if (titleMatch) {
return titleMatch[1].trim().replace(/^['"]|['"]$/g, "");
}
const h1Match = raw.match(/^#\s+(.+)$/m);
if (h1Match) {
return h1Match[1].trim();
}
return path.basename(filePath, ".md");
}
function getCategory(filePath) {
return path.basename(path.dirname(filePath));
}
function getSafeCoverName(filePath) {
if (existingCoverMap.has(filePath)) {
return existingCoverMap.get(filePath);
}
const baseName = path.basename(filePath, ".md");
const safeName = baseName.replace(/[<>:"/\\|?*]/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
return `${safeName}-cover.svg`;
}
function splitTitle(title) {
const clean = title.trim();
if (clean.length <= 14) return [clean];
if (clean.length <= 28) return [clean.slice(0, Math.ceil(clean.length / 2)), clean.slice(Math.ceil(clean.length / 2))];
const first = clean.slice(0, 12);
const second = clean.slice(12, 24);
const third = clean.slice(24);
return [first, second, third].filter(Boolean);
}
function getTheme(category, title) {
const theme = {
key: "default",
label: "博客封面",
subtitle: "知识沉淀 / 主题文章",
bg1: "#0B1020",
bg2: "#172554",
bg3: "#0F766E",
accent1: "#67E8F9",
accent2: "#A78BFA",
accent3: "#F472B6",
panel: "rgba(7,12,28,0.70)",
tags: ["博客", "封面", "文章"],
};
const categoryThemes = {
"AI与大模型": {
key: "ai",
label: "AI 与大模型",
subtitle: "模型能力 / Agent / 智能协作",
bg1: "#0B1020",
bg2: "#1E1B4B",
bg3: "#0F766E",
accent1: "#67E8F9",
accent2: "#A855F7",
accent3: "#22D3EE",
tags: ["AI", "模型", "智能"],
},
"机器学习": {
key: "ml",
label: "机器学习",
subtitle: "算法原理 / 神经网络 / 模型理解",
bg1: "#0B1020",
bg2: "#172554",
bg3: "#4C1D95",
accent1: "#67E8F9",
accent2: "#C084FC",
accent3: "#F472B6",
tags: ["学习", "网络", "模型"],
},
"编程与工具": {
key: "tools",
label: "编程与工具",
subtitle: "工程实践 / 工具链 / 效率提升",
bg1: "#071A33",
bg2: "#0F3D74",
bg3: "#0B7285",
accent1: "#7DD3FC",
accent2: "#FDBA74",
accent3: "#60A5FA",
tags: ["工具", "工程", "实践"],
},
"数据分析与报告": {
key: "report",
label: "数据分析与报告",
subtitle: "指标洞察 / 数据审查 / 结果汇总",
bg1: "#111827",
bg2: "#1F2937",
bg3: "#0F766E",
accent1: "#34D399",
accent2: "#2DD4BF",
accent3: "#A7F3D0",
tags: ["数据", "分析", "报告"],
},
"学术与效率": {
key: "academic",
label: "学术与效率",
subtitle: "论文写作 / 知识表达 / 工具方法",
bg1: "#1F172A",
bg2: "#312E81",
bg3: "#0F766E",
accent1: "#C4B5FD",
accent2: "#93C5FD",
accent3: "#A7F3D0",
tags: ["学术", "写作", "效率"],
},
其他: {
key: "outline",
label: "内容策划",
subtitle: "选题梳理 / 结构设计 / 创作准备",
bg1: "#0F172A",
bg2: "#1E293B",
bg3: "#334155",
accent1: "#93C5FD",
accent2: "#C4B5FD",
accent3: "#F9A8D4",
tags: ["策划", "结构", "草稿"],
},
};
Object.assign(theme, categoryThemes[category] || {});
const keywordRules = [
[/Claude|Vibe|CLI|Agent/i, { subtitle: "AI 编程 / Agentic CLI / 智能协作", tags: ["Agent", "CLI", "AI"] }],
[/DeepSeek|大模型/, { subtitle: "模型解析 / 趋势洞察 / 架构思考", tags: ["模型", "解析", "趋势"] }],
[/Docker|部署/, { subtitle: "容器部署 / 服务编排 / 工程实战", tags: ["部署", "容器", "实战"] }],
[/Git/, { subtitle: "版本控制 / 团队协作 / 工作流", tags: ["Git", "协作", "工作流"] }],
[/OpenClaw/, { subtitle: "AI 编程助手 / 安装配置 / 使用实践", tags: ["AI 工具", "安装", "指南"] }],
[/\buv\b|uv工具/, { subtitle: "Python 工具链 / 依赖管理 / 开发效率", tags: ["Python", "uv", "效率"] }],
[/SEO|爬取|摘要|标签|文章列表|技术栈/, { subtitle: "数据整理 / 指标审查 / 结果报告", tags: ["数据", "报告", "审查"] }],
[/LaTeX|论文/, { subtitle: "论文写作 / 排版工具 / 学术效率", tags: ["论文", "LaTeX", "效率"] }],
[/LinearRegression|线性回归/, { subtitle: "监督学习 / 回归模型 / 入门理解", tags: ["回归", "基础", "算法"] }],
[/卷积|全连接层/, { subtitle: "神经网络 / 表征学习 / 结构演进", tags: ["卷积", "神经网络", "视觉"] }],
[/深度学习/, { subtitle: "模型体系 / 核心概念 / 系统学习", tags: ["深度学习", "模型", "指南"] }],
[/视觉语言模型/, { subtitle: "多模态 AI / 视觉理解 / 语言推理", tags: ["VLM", "多模态", "推理"] }],
[/大纲/, { subtitle: "内容策划 / 结构拆解 / 写作准备", tags: ["大纲", "结构", "规划"] }],
];
for (const [regex, patch] of keywordRules) {
if (regex.test(title)) {
Object.assign(theme, patch);
break;
}
}
return theme;
}
function getGraphicMarkup(theme) {
const { key, accent1, accent2, accent3 } = theme;
if (key === "ai") {
return `
`;
}
if (key === "ml") {
return `
`;
}
if (key === "tools") {
return `
`;
}
if (key === "report") {
return `
`;
}
if (key === "academic") {
return `
∫
LaTeX
Σ x² + y²
`;
}
return `
`;
}
function buildTitleMarkup(title) {
const lines = splitTitle(title);
const fontSize = lines.length === 1 ? 48 : lines.length === 2 ? 44 : 36;
const lineHeight = lines.length === 3 ? 48 : 56;
const startY = lines.length === 1 ? 250 : lines.length === 2 ? 224 : 196;
return lines
.map((line, index) => {
const y = startY + index * lineHeight;
return ` ${escapeXml(line)}`;
})
.join("\n");
}
function buildTagMarkup(tags) {
const fills = ["rgba(103,232,249,0.12)", "rgba(168,85,247,0.12)", "rgba(244,114,182,0.12)"];
const strokes = ["rgba(103,232,249,0.30)", "rgba(196,181,253,0.30)", "rgba(251,207,232,0.26)"];
const textColors = ["#BAE6FD", "#DDD6FE", "#FBCFE8"];
let x = 112;
return tags.slice(0, 3).map((tag, index) => {
const width = Math.max(118, 54 + tag.length * 22);
const rect = ` `;
const text = ` ${escapeXml(tag)}`;
x += width + 18;
return `${rect}\n${text}`;
}).join("\n");
}
function buildSvg(title, theme) {
return `
`;
}
const articles = walkMarkdownFiles(blogRoot);
const created = [];
const skipped = [];
for (const articlePath of articles) {
if (existingCoverMap.has(articlePath)) {
skipped.push(`${articlePath} => ${existingCoverMap.get(articlePath)}`);
continue;
}
const title = getArticleTitle(articlePath);
const category = getCategory(articlePath);
const theme = getTheme(category, title);
const coverName = getSafeCoverName(articlePath);
const coverPath = path.join(coverDir, coverName);
if (fs.existsSync(coverPath)) {
skipped.push(`${articlePath} => ${coverName}`);
continue;
}
fs.writeFileSync(coverPath, buildSvg(title, theme), "utf8");
created.push(coverPath);
}
console.log(`Created: ${created.length}`);
for (const file of created) console.log(file);
console.log(`Skipped: ${skipped.length}`);
for (const file of skipped) console.log(file);