04d899ca89
为博客系统添加了多篇技术文章的封面图片,涵盖Git、Python工具、AI大模型、机器学习等主题。这些封面采用统一的SVG格式设计,包含标题、分类标签和视觉元素,用于提升博客文章的可视化展示效果。
378 lines
16 KiB
JavaScript
378 lines
16 KiB
JavaScript
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 `
|
||
<g transform="translate(760 112)">
|
||
<circle cx="200" cy="182" r="168" fill="rgba(9,14,32,0.42)" stroke="rgba(255,255,255,0.16)" />
|
||
<circle cx="200" cy="182" r="120" fill="rgba(103,232,249,0.10)" stroke="${accent1}" stroke-width="3" />
|
||
<circle cx="200" cy="182" r="68" fill="rgba(168,85,247,0.18)" stroke="${accent2}" stroke-width="3" />
|
||
<circle cx="200" cy="182" r="18" fill="#E9D5FF" />
|
||
<path d="M28 124C92 148 134 166 198 182" stroke="${accent1}" stroke-width="7" stroke-linecap="round" />
|
||
<path d="M6 190C82 194 132 190 198 182" stroke="${accent2}" stroke-width="7" stroke-linecap="round" />
|
||
<path d="M18 260C94 232 134 214 198 182" stroke="${accent3}" stroke-width="7" stroke-linecap="round" />
|
||
<circle cx="24" cy="124" r="18" fill="#E0F2FE" />
|
||
<circle cx="10" cy="190" r="18" fill="#DDD6FE" />
|
||
<circle cx="24" cy="260" r="18" fill="#CCFBF1" />
|
||
<path d="M312 88L390 126L422 208L382 288L304 330" stroke="${accent2}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||
<circle cx="312" cy="88" r="14" fill="#C4B5FD" />
|
||
<circle cx="390" cy="126" r="14" fill="#93C5FD" />
|
||
<circle cx="422" cy="208" r="14" fill="#A7F3D0" />
|
||
<circle cx="382" cy="288" r="14" fill="#67E8F9" />
|
||
<circle cx="304" cy="330" r="14" fill="#F9A8D4" />
|
||
</g>`;
|
||
}
|
||
if (key === "ml") {
|
||
return `
|
||
<g transform="translate(760 112)">
|
||
<rect x="0" y="72" width="116" height="232" rx="24" fill="rgba(103,232,249,0.14)" stroke="${accent1}" />
|
||
<rect x="154" y="24" width="116" height="328" rx="24" fill="rgba(192,132,252,0.14)" stroke="${accent2}" />
|
||
<rect x="308" y="96" width="116" height="184" rx="24" fill="rgba(244,114,182,0.14)" stroke="${accent3}" />
|
||
<circle cx="58" cy="130" r="12" fill="#E0F2FE" />
|
||
<circle cx="58" cy="188" r="12" fill="#BAE6FD" />
|
||
<circle cx="58" cy="246" r="12" fill="#C4B5FD" />
|
||
<circle cx="212" cy="116" r="12" fill="#E9D5FF" />
|
||
<circle cx="212" cy="178" r="12" fill="#DDD6FE" />
|
||
<circle cx="212" cy="240" r="12" fill="#F5D0FE" />
|
||
<circle cx="212" cy="302" r="12" fill="#FBCFE8" />
|
||
<circle cx="366" cy="146" r="12" fill="#FCE7F3" />
|
||
<circle cx="366" cy="198" r="12" fill="#FBCFE8" />
|
||
<circle cx="366" cy="250" r="12" fill="#F9A8D4" />
|
||
<path d="M70 130L200 116L354 146" stroke="${accent1}" stroke-width="4" />
|
||
<path d="M70 188L200 178L354 198" stroke="${accent2}" stroke-width="4" />
|
||
<path d="M70 246L200 240L354 250" stroke="${accent3}" stroke-width="4" />
|
||
<path d="M0 382C84 322 178 302 278 314C336 320 390 340 444 382" stroke="${accent1}" stroke-width="6" stroke-linecap="round" />
|
||
</g>`;
|
||
}
|
||
if (key === "tools") {
|
||
return `
|
||
<g transform="translate(760 108)">
|
||
<path d="M148 0L296 86V258L148 344L0 258V86L148 0Z" fill="rgba(8,47,73,0.42)" stroke="rgba(255,255,255,0.16)" />
|
||
<path d="M148 26L268 96L148 166L28 96L148 26Z" fill="${accent1}" />
|
||
<path d="M28 96L148 166V300L28 230V96Z" fill="#0284C7" />
|
||
<path d="M268 96L148 166V300L268 230V96Z" fill="#0369A1" />
|
||
<path d="M356 112H470" stroke="${accent2}" stroke-width="10" stroke-linecap="round" />
|
||
<path d="M356 176H506" stroke="${accent1}" stroke-width="10" stroke-linecap="round" />
|
||
<path d="M356 240H454" stroke="${accent3}" stroke-width="10" stroke-linecap="round" />
|
||
<rect x="352" y="54" width="176" height="246" rx="28" fill="rgba(15,23,42,0.34)" stroke="rgba(255,255,255,0.12)" />
|
||
</g>`;
|
||
}
|
||
if (key === "report") {
|
||
return `
|
||
<g transform="translate(760 116)">
|
||
<rect x="0" y="24" width="430" height="300" rx="28" fill="rgba(8,47,73,0.36)" stroke="rgba(255,255,255,0.14)" />
|
||
<path d="M54 248V136" stroke="${accent1}" stroke-width="28" stroke-linecap="round" />
|
||
<path d="M134 248V92" stroke="${accent2}" stroke-width="28" stroke-linecap="round" />
|
||
<path d="M214 248V164" stroke="${accent3}" stroke-width="28" stroke-linecap="round" />
|
||
<path d="M294 248V68" stroke="#6EE7B7" stroke-width="28" stroke-linecap="round" />
|
||
<path d="M374 248V118" stroke="#99F6E4" stroke-width="28" stroke-linecap="round" />
|
||
<path d="M44 286H388" stroke="rgba(255,255,255,0.16)" stroke-width="4" />
|
||
<circle cx="370" cy="94" r="56" fill="rgba(167,243,208,0.10)" stroke="${accent3}" stroke-width="4" />
|
||
<path d="M370 94L398 66" stroke="${accent1}" stroke-width="8" stroke-linecap="round" />
|
||
<path d="M370 94L370 146" stroke="${accent2}" stroke-width="8" stroke-linecap="round" />
|
||
</g>`;
|
||
}
|
||
if (key === "academic") {
|
||
return `
|
||
<g transform="translate(768 106)">
|
||
<rect x="0" y="0" width="272" height="372" rx="24" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.14)" />
|
||
<rect x="34" y="48" width="204" height="18" rx="9" fill="${accent1}" />
|
||
<rect x="34" y="92" width="184" height="12" rx="6" fill="rgba(255,255,255,0.56)" />
|
||
<rect x="34" y="122" width="208" height="12" rx="6" fill="rgba(255,255,255,0.56)" />
|
||
<rect x="34" y="152" width="168" height="12" rx="6" fill="rgba(255,255,255,0.56)" />
|
||
<path d="M338 84C384 30 468 32 512 88C558 146 540 236 472 280C396 330 302 302 270 236" stroke="${accent2}" stroke-width="8" stroke-linecap="round" />
|
||
<text x="320" y="168" fill="#E0E7FF" font-family="Cambria Math, Times New Roman, serif" font-size="58">∫</text>
|
||
<text x="386" y="158" fill="#DBEAFE" font-family="Cambria Math, Times New Roman, serif" font-size="42">LaTeX</text>
|
||
<text x="352" y="228" fill="#CCFBF1" font-family="Cambria Math, Times New Roman, serif" font-size="40">Σ x² + y²</text>
|
||
</g>`;
|
||
}
|
||
return `
|
||
<g transform="translate(760 116)">
|
||
<rect x="0" y="0" width="448" height="296" rx="30" fill="rgba(15,23,42,0.34)" stroke="rgba(255,255,255,0.14)" />
|
||
<path d="M34 250C108 164 182 124 268 122C332 120 390 140 432 178" stroke="${accent1}" stroke-width="8" stroke-linecap="round" />
|
||
<path d="M34 202C108 140 194 110 286 118C350 124 404 148 442 184" stroke="${accent2}" stroke-width="8" stroke-linecap="round" />
|
||
<path d="M34 154C100 116 168 96 244 96C322 96 394 118 448 160" stroke="${accent3}" stroke-width="8" stroke-linecap="round" />
|
||
<circle cx="110" cy="84" r="22" fill="#93C5FD" />
|
||
<circle cx="220" cy="62" r="18" fill="#C4B5FD" />
|
||
<circle cx="342" cy="72" r="16" fill="#F9A8D4" />
|
||
</g>`;
|
||
}
|
||
|
||
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 ` <text x="112" y="${y}" fill="#F8FAFC" font-family="Microsoft YaHei, Segoe UI, sans-serif" font-size="${fontSize}" font-weight="700">${escapeXml(line)}</text>`;
|
||
})
|
||
.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 = ` <rect x="${x}" y="376" width="${width}" height="56" rx="28" fill="${fills[index]}" stroke="${strokes[index]}" />`;
|
||
const text = ` <text x="${x + 34}" y="412" fill="${textColors[index]}" font-family="Microsoft YaHei, Segoe UI, sans-serif" font-size="22">${escapeXml(tag)}</text>`;
|
||
x += width + 18;
|
||
return `${rect}\n${text}`;
|
||
}).join("\n");
|
||
}
|
||
|
||
function buildSvg(title, theme) {
|
||
return `<svg width="1280" height="720" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<linearGradient id="bg" x1="120" y1="40" x2="1180" y2="680" gradientUnits="userSpaceOnUse">
|
||
<stop stop-color="${theme.bg1}" />
|
||
<stop offset="0.5" stop-color="${theme.bg2}" />
|
||
<stop offset="1" stop-color="${theme.bg3}" />
|
||
</linearGradient>
|
||
</defs>
|
||
<rect width="1280" height="720" fill="url(#bg)" />
|
||
<circle cx="1090" cy="110" r="176" fill="${theme.accent1}" fill-opacity="0.12" />
|
||
<circle cx="980" cy="590" r="220" fill="${theme.accent2}" fill-opacity="0.14" />
|
||
<circle cx="258" cy="612" r="240" fill="${theme.accent3}" fill-opacity="0.10" />
|
||
<rect x="72" y="84" width="586" height="552" rx="32" fill="${theme.panel}" stroke="rgba(255,255,255,0.12)" />
|
||
<text x="112" y="176" fill="${theme.accent1}" font-family="Microsoft YaHei, Segoe UI, sans-serif" font-size="24" letter-spacing="4">${escapeXml(theme.label)}</text>
|
||
${buildTitleMarkup(title)}
|
||
<text x="112" y="324" fill="#E2E8F0" font-family="Microsoft YaHei, Segoe UI, sans-serif" font-size="26">${escapeXml(theme.subtitle)}</text>
|
||
${buildTagMarkup(theme.tags)}
|
||
<path d="M112 494H564" stroke="rgba(255,255,255,0.14)" stroke-width="2" />
|
||
<path d="M112 536H526" stroke="${theme.accent1}" stroke-width="8" stroke-linecap="round" />
|
||
<path d="M112 576H458" stroke="${theme.accent2}" stroke-width="8" stroke-linecap="round" />
|
||
${getGraphicMarkup(theme)}
|
||
</svg>
|
||
`;
|
||
}
|
||
|
||
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);
|