Files
Obsidian/博客/其他/generate-blog-covers.ps1
Serendipity 04d899ca89 feat(博客): 添加多篇博客封面图片
为博客系统添加了多篇技术文章的封面图片,涵盖Git、Python工具、AI大模型、机器学习等主题。这些封面采用统一的SVG格式设计,包含标题、分类标签和视觉元素,用于提升博客文章的可视化展示效果。
2026-05-04 22:56:16 +08:00

495 lines
18 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 批量为博客文章生成 SVG 封面图。
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$blogRoot = "d:\Code\Obsidian\博客"
$coverDir = Join-Path $blogRoot "covers"
New-Item -ItemType Directory -Force -Path $coverDir | Out-Null
# 已人工调整过的样例封面,后续批量任务中不覆盖。
$existingCoverMap = @{
"d:\Code\Obsidian\博客\AI与大模型\深入解析 Claude CodeVibe Coding 时代的 AI 编程利器.md" = "claude-code-vibe-coding-cover.svg"
"d:\Code\Obsidian\博客\编程与工具\Docker部署完全指南.md" = "docker-deployment-guide-cover.svg"
"d:\Code\Obsidian\博客\机器学习\视觉语言模型技术综述.md" = "vision-language-model-cover.svg"
}
function Escape-Xml {
param([string]$Text)
if ([string]::IsNullOrEmpty($Text)) {
return ""
}
return $Text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace('"', "&quot;")
}
function Get-ArticleTitle {
param([string]$Path)
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
if ($raw -match "(?m)^title:\s*(.+)$") {
return $matches[1].Trim().Trim("'").Trim('"')
}
if ($raw -match "(?m)^#\s+(.+)$") {
return $matches[1].Trim()
}
return [System.IO.Path]::GetFileNameWithoutExtension($Path)
}
function Get-CategoryName {
param([string]$Path)
return Split-Path (Split-Path $Path -Parent) -Leaf
}
function Get-SafeCoverName {
param([string]$ArticlePath)
if ($existingCoverMap.ContainsKey($ArticlePath)) {
return $existingCoverMap[$ArticlePath]
}
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($ArticlePath)
$safeName = ($baseName -replace '[<>:"/\\|?*]', "-") -replace "\s+", "-"
$safeName = $safeName.Trim("-")
return "$safeName-cover.svg"
}
function Split-TextLines {
param(
[string]$Text,
[int]$MaxCharsPerLine
)
$clean = $Text.Trim()
if ([string]::IsNullOrWhiteSpace($clean)) {
return @("")
}
$lines = New-Object System.Collections.Generic.List[string]
$remaining = $clean
while ($remaining.Length -gt $MaxCharsPerLine) {
$cutIndex = $MaxCharsPerLine
$spaceIndex = $remaining.LastIndexOf(" ", [Math]::Min($MaxCharsPerLine, $remaining.Length - 1))
if ($spaceIndex -gt 6) {
$cutIndex = $spaceIndex
}
$lines.Add($remaining.Substring(0, $cutIndex).Trim())
$remaining = $remaining.Substring($cutIndex).Trim()
}
if ($remaining.Length -gt 0) {
$lines.Add($remaining)
}
if ($lines.Count -gt 3) {
$merged = @($lines[0], $lines[1], (($lines | Select-Object -Skip 2) -join " "))
return $merged
}
return $lines.ToArray()
}
function Get-ThemeInfo {
param(
[string]$Category,
[string]$Title
)
$theme = [ordered]@{
Key = "default"
Label = "博客封面"
Subtitle = "知识沉淀 / 主题文章"
Bg1 = "#0B1020"
Bg2 = "#172554"
Bg3 = "#0F766E"
Accent1 = "#67E8F9"
Accent2 = "#A78BFA"
Accent3 = "#F472B6"
Panel = "rgba(7,12,28,0.70)"
Tags = @("博客", "封面", "文章")
}
switch ($Category) {
"AI与大模型" {
$theme.Key = "ai"
$theme.Label = "AI 与大模型"
$theme.Subtitle = "模型能力 / Agent / 智能协作"
$theme.Bg1 = "#0B1020"
$theme.Bg2 = "#1E1B4B"
$theme.Bg3 = "#0F766E"
$theme.Accent1 = "#67E8F9"
$theme.Accent2 = "#A855F7"
$theme.Accent3 = "#22D3EE"
$theme.Tags = @("AI", "模型", "智能")
}
"机器学习" {
$theme.Key = "ml"
$theme.Label = "机器学习"
$theme.Subtitle = "算法原理 / 神经网络 / 模型理解"
$theme.Bg1 = "#0B1020"
$theme.Bg2 = "#172554"
$theme.Bg3 = "#4C1D95"
$theme.Accent1 = "#67E8F9"
$theme.Accent2 = "#C084FC"
$theme.Accent3 = "#F472B6"
$theme.Tags = @("学习", "网络", "模型")
}
"编程与工具" {
$theme.Key = "tools"
$theme.Label = "编程与工具"
$theme.Subtitle = "工程实践 / 工具链 / 效率提升"
$theme.Bg1 = "#071A33"
$theme.Bg2 = "#0F3D74"
$theme.Bg3 = "#0B7285"
$theme.Accent1 = "#7DD3FC"
$theme.Accent2 = "#FDBA74"
$theme.Accent3 = "#60A5FA"
$theme.Tags = @("工具", "工程", "实践")
}
"数据分析与报告" {
$theme.Key = "report"
$theme.Label = "数据分析与报告"
$theme.Subtitle = "指标洞察 / 数据审查 / 结果汇总"
$theme.Bg1 = "#111827"
$theme.Bg2 = "#1F2937"
$theme.Bg3 = "#0F766E"
$theme.Accent1 = "#34D399"
$theme.Accent2 = "#2DD4BF"
$theme.Accent3 = "#A7F3D0"
$theme.Tags = @("数据", "分析", "报告")
}
"学术与效率" {
$theme.Key = "academic"
$theme.Label = "学术与效率"
$theme.Subtitle = "论文写作 / 知识表达 / 工具方法"
$theme.Bg1 = "#1F172A"
$theme.Bg2 = "#312E81"
$theme.Bg3 = "#0F766E"
$theme.Accent1 = "#C4B5FD"
$theme.Accent2 = "#93C5FD"
$theme.Accent3 = "#A7F3D0"
$theme.Tags = @("学术", "写作", "效率")
}
"其他" {
$theme.Key = "outline"
$theme.Label = "内容策划"
$theme.Subtitle = "选题梳理 / 结构设计 / 创作准备"
$theme.Bg1 = "#0F172A"
$theme.Bg2 = "#1E293B"
$theme.Bg3 = "#334155"
$theme.Accent1 = "#93C5FD"
$theme.Accent2 = "#C4B5FD"
$theme.Accent3 = "#F9A8D4"
$theme.Tags = @("策划", "结构", "草稿")
}
}
switch -Regex ($Title) {
"Claude|Vibe|CLI|Agent" {
$theme.Subtitle = "AI 编程 / Agentic CLI / 智能协作"
$theme.Tags = @("Agent", "CLI", "AI")
break
}
"DeepSeek|大模型" {
$theme.Subtitle = "模型解析 / 趋势洞察 / 架构思考"
$theme.Tags = @("模型", "解析", "趋势")
break
}
"Docker|部署" {
$theme.Subtitle = "容器部署 / 服务编排 / 工程实战"
$theme.Tags = @("部署", "容器", "实战")
break
}
"Git" {
$theme.Subtitle = "版本控制 / 团队协作 / 工作流"
$theme.Tags = @("Git", "协作", "工作流")
break
}
"OpenClaw" {
$theme.Subtitle = "AI 编程助手 / 安装配置 / 使用实践"
$theme.Tags = @("AI 工具", "安装", "指南")
break
}
"uv" {
$theme.Subtitle = "Python 工具链 / 依赖管理 / 开发效率"
$theme.Tags = @("Python", "uv", "效率")
break
}
"SEO|爬取|摘要|标签|文章列表|技术栈" {
$theme.Subtitle = "数据整理 / 指标审查 / 结果报告"
$theme.Tags = @("数据", "报告", "审查")
break
}
"LaTeX|论文" {
$theme.Subtitle = "论文写作 / 排版工具 / 学术效率"
$theme.Tags = @("论文", "LaTeX", "效率")
break
}
"LinearRegression|线性回归" {
$theme.Subtitle = "监督学习 / 回归模型 / 入门理解"
$theme.Tags = @("回归", "基础", "算法")
break
}
"卷积|全连接层" {
$theme.Subtitle = "神经网络 / 表征学习 / 结构演进"
$theme.Tags = @("卷积", "神经网络", "视觉")
break
}
"深度学习" {
$theme.Subtitle = "模型体系 / 核心概念 / 系统学习"
$theme.Tags = @("深度学习", "模型", "指南")
break
}
"视觉语言模型" {
$theme.Subtitle = "多模态 AI / 视觉理解 / 语言推理"
$theme.Tags = @("VLM", "多模态", "推理")
break
}
"大纲" {
$theme.Subtitle = "内容策划 / 结构拆解 / 写作准备"
$theme.Tags = @("大纲", "结构", "规划")
break
}
}
return $theme
}
function Get-GraphicMarkup {
param($Theme)
switch ($Theme.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="$($Theme.Accent1)" stroke-width="3" />
<circle cx="200" cy="182" r="68" fill="rgba(168,85,247,0.18)" stroke="$($Theme.Accent2)" stroke-width="3" />
<circle cx="200" cy="182" r="18" fill="#E9D5FF" />
<path d="M28 124C92 148 134 166 198 182" stroke="$($Theme.Accent1)" stroke-width="7" stroke-linecap="round" />
<path d="M6 190C82 194 132 190 198 182" stroke="$($Theme.Accent2)" stroke-width="7" stroke-linecap="round" />
<path d="M18 260C94 232 134 214 198 182" stroke="$($Theme.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="$($Theme.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>
"@
}
"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="$($Theme.Accent1)" />
<rect x="154" y="24" width="116" height="328" rx="24" fill="rgba(192,132,252,0.14)" stroke="$($Theme.Accent2)" />
<rect x="308" y="96" width="116" height="184" rx="24" fill="rgba(244,114,182,0.14)" stroke="$($Theme.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="$($Theme.Accent1)" stroke-width="4" />
<path d="M70 188L200 178L354 198" stroke="$($Theme.Accent2)" stroke-width="4" />
<path d="M70 246L200 240L354 250" stroke="$($Theme.Accent3)" stroke-width="4" />
<path d="M0 382C84 322 178 302 278 314C336 320 390 340 444 382" stroke="$($Theme.Accent1)" stroke-width="6" stroke-linecap="round" />
</g>
"@
}
"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="$($Theme.Accent1)" />
<path d="M28 96L148 166V300L28 230V96Z" fill="#0284C7" />
<path d="M268 96L148 166V300L268 230V96Z" fill="#0369A1" />
<path d="M356 112H470" stroke="$($Theme.Accent2)" stroke-width="10" stroke-linecap="round" />
<path d="M356 176H506" stroke="$($Theme.Accent1)" stroke-width="10" stroke-linecap="round" />
<path d="M356 240H454" stroke="$($Theme.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>
"@
}
"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="$($Theme.Accent1)" stroke-width="28" stroke-linecap="round" />
<path d="M134 248V92" stroke="$($Theme.Accent2)" stroke-width="28" stroke-linecap="round" />
<path d="M214 248V164" stroke="$($Theme.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="$($Theme.Accent3)" stroke-width="4" />
<path d="M370 94L398 66" stroke="$($Theme.Accent1)" stroke-width="8" stroke-linecap="round" />
<path d="M370 94L370 146" stroke="$($Theme.Accent2)" stroke-width="8" stroke-linecap="round" />
</g>
"@
}
"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="$($Theme.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="$($Theme.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>
"@
}
default {
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="$($Theme.Accent1)" stroke-width="8" stroke-linecap="round" />
<path d="M34 202C108 140 194 110 286 118C350 124 404 148 442 184" stroke="$($Theme.Accent2)" stroke-width="8" stroke-linecap="round" />
<path d="M34 154C100 116 168 96 244 96C322 96 394 118 448 160" stroke="$($Theme.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 New-TitleMarkup {
param([string]$Title)
$maxChars = if ($Title.Length -gt 28) { 14 } elseif ($Title.Length -gt 20) { 16 } else { 20 }
$lines = Split-TextLines -Text $Title -MaxCharsPerLine $maxChars
$fontSize = switch ($lines.Count) {
1 { 48 }
2 { if (($lines | Measure-Object -Maximum Length).Maximum -gt 18) { 42 } else { 46 } }
default { 36 }
}
$lineHeight = if ($lines.Count -ge 3) { 48 } else { 56 }
$startY = switch ($lines.Count) {
1 { 250 }
2 { 226 }
default { 198 }
}
$markup = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $lines.Count; $i++) {
$y = $startY + ($i * $lineHeight)
$markup.Add(" <text x=""112"" y=""$y"" fill=""#F8FAFC"" font-family=""Microsoft YaHei, Segoe UI, sans-serif"" font-size=""$fontSize"" font-weight=""700"">$([string](Escape-Xml $lines[$i]))</text>")
}
return ($markup -join "`n")
}
function New-TagMarkup {
param([string[]]$Tags)
$x = 112
$y = 376
$items = New-Object System.Collections.Generic.List[string]
$fills = @("rgba(103,232,249,0.12)", "rgba(168,85,247,0.12)", "rgba(244,114,182,0.12)")
$strokes = @("rgba(103,232,249,0.30)", "rgba(196,181,253,0.30)", "rgba(251,207,232,0.26)")
$texts = @("#BAE6FD", "#DDD6FE", "#FBCFE8")
for ($i = 0; $i -lt [Math]::Min(3, $Tags.Count); $i++) {
$tag = $Tags[$i]
$width = [Math]::Max(118, 54 + ($tag.Length * 22))
$items.Add(" <rect x=""$x"" y=""$y"" width=""$width"" height=""56"" rx=""28"" fill=""$($fills[$i])"" stroke=""$($strokes[$i])"" />")
$items.Add(" <text x=""$($x + 34)"" y=""$($y + 36)"" fill=""$($texts[$i])"" font-family=""Microsoft YaHei, Segoe UI, sans-serif"" font-size=""22"">$([string](Escape-Xml $tag))</text>")
$x += $width + 18
}
return ($items -join "`n")
}
function New-CoverSvg {
param(
[string]$Title,
[hashtable]$Theme
)
$label = Escape-Xml $Theme.Label
$subtitle = Escape-Xml $Theme.Subtitle
$titleMarkup = New-TitleMarkup -Title $Title
$tagMarkup = New-TagMarkup -Tags $Theme.Tags
$graphicMarkup = Get-GraphicMarkup -Theme $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">$label</text>
$titleMarkup
<text x="112" y="324" fill="#E2E8F0" font-family="Microsoft YaHei, Segoe UI, sans-serif" font-size="26">$subtitle</text>
$tagMarkup
<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" />
$graphicMarkup
</svg>
"@
}
$articles = Get-ChildItem -LiteralPath $blogRoot -Recurse -File -Filter "*.md" |
Where-Object { $_.FullName -notlike "*\covers\*" } |
Sort-Object FullName
$created = New-Object System.Collections.Generic.List[string]
$skipped = New-Object System.Collections.Generic.List[string]
foreach ($article in $articles) {
$articlePath = $article.FullName
if ($existingCoverMap.ContainsKey($articlePath)) {
$skipped.Add("$articlePath => $($existingCoverMap[$articlePath])")
continue
}
$title = Get-ArticleTitle -Path $articlePath
$category = Get-CategoryName -Path $articlePath
$theme = Get-ThemeInfo -Category $category -Title $title
$coverName = Get-SafeCoverName -ArticlePath $articlePath
$coverPath = Join-Path $coverDir $coverName
if (Test-Path -LiteralPath $coverPath) {
$skipped.Add("$articlePath => $(Split-Path $coverPath -Leaf)")
continue
}
$svg = New-CoverSvg -Title $title -Theme $theme
Set-Content -LiteralPath $coverPath -Value $svg -Encoding UTF8
$created.Add($coverPath)
}
Write-Output "Created: $($created.Count)"
$created | ForEach-Object { Write-Output $_ }
Write-Output "Skipped: $($skipped.Count)"
$skipped | ForEach-Object { Write-Output $_ }