# 批量为博客文章生成 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 Code:Vibe 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("&", "&").Replace("<", "<").Replace(">", ">").Replace('"', """)
}
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 @"
"@
}
"ml" {
return @"
"@
}
"tools" {
return @"
"@
}
"report" {
return @"
"@
}
"academic" {
return @"
∫
LaTeX
Σ x² + y²
"@
}
default {
return @"
"@
}
}
}
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(" $([string](Escape-Xml $lines[$i]))")
}
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(" ")
$items.Add(" $([string](Escape-Xml $tag))")
$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 @"
"@
}
$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 $_ }