# 批量为博客文章生成 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 @" $label $titleMarkup $subtitle $tagMarkup $graphicMarkup "@ } $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 $_ }