Files
reverse-skill/skills/scripts/extract-summaries.ps1
2026-08-08 18:32:33 +08:00

125 lines
5.5 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.
#Requires -Version 5.1
# 生成 skills/INDEX.md:从各模块 SKILL.md 的 frontmatter 提取 name+description,生成导航索引。
# 幂等:重复运行输出一致(CI 用 git diff 校验防 drift)。
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/extract-summaries.ps1
# powershell -File skills/scripts/extract-summaries.ps1 -Check # 只校验不写(CI 模式,不一致 exit 1)
param(
[switch] $Check
)
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$indexPath = Join-Path $skillsRoot 'INDEX.md'
# 扫描版本库中的模块 SKILL.md(排除本地 ignored/private trees,保证 clean clone 与开发机幂等)。
$skipDirs = @('ops', 'scripts', 'config', 'tests', 'field-journal', 'references')
$packageRoot = Split-Path -Parent $skillsRoot
$trackedSkillFiles = @()
$git = Get-Command git -ErrorAction SilentlyContinue
if ($git) {
$trackedPaths = @(& $git.Source -C $packageRoot ls-files -- 'skills/**/SKILL.md')
if ($LASTEXITCODE -eq 0) {
$trackedSkillFiles = @($trackedPaths | ForEach-Object {
$fullPath = Join-Path $packageRoot ($_ -replace '/', [IO.Path]::DirectorySeparatorChar)
if (Test-Path -LiteralPath $fullPath -PathType Leaf) { Get-Item -LiteralPath $fullPath }
})
}
}
$candidateSkillFiles = if ($trackedSkillFiles.Count -gt 0) {
$trackedSkillFiles
} else {
@(Get-ChildItem -Path $skillsRoot -Recurse -Filter 'SKILL.md')
}
$skillFiles = $candidateSkillFiles | Where-Object {
$rel = $_.FullName.Substring($skillsRoot.Length + 1)
$rel -ne 'SKILL.md' -and -not ($skipDirs | Where-Object { $rel.StartsWith($_ + '\') -or $rel.StartsWith($_ + '/') })
} | Sort-Object { $_.FullName.Substring($skillsRoot.Length + 1) }
$rows = New-Object System.Collections.ArrayList
foreach ($sf in $skillFiles) {
$rel = $sf.FullName.Substring($skillsRoot.Length + 1)
# 兼容 Windows(\)与 Linux/macOS(/)两种路径分隔符
$dir = $rel.Split(@('\', '/'))[0]
$head = Get-Content -LiteralPath $sf.FullName -TotalCount 15 -Encoding UTF8
$name = ''; $desc = ''; $blockMode = $false; $inFm = $false
foreach ($line in $head) {
if ($line -match '^---') {
if ($inFm) { break } # 第二个 --- 结束 frontmatter
$inFm = $true; continue
}
if (-not $inFm) { continue }
if ($line -match '^name:\s*(.+)$') { $name = $Matches[1].Trim(); continue }
if ($line -match '^description:\s*\|') { $blockMode = $true; continue }
if ($line -match '^description:\s*(.+)$') { $desc = $Matches[1].Trim(); continue }
if ($blockMode) {
# YAML 块:缩进的文本行拼接(取首行,超长截断)
if ($line -match '^\s{2,}(.+)$') {
if (-not $desc) { $desc = $Matches[1].Trim() }
} elseif ($line -match '^\S') { $blockMode = $false }
}
}
if (-not $name) { $name = $dir }
if (-not $desc) { $desc = '(无摘要)' }
if ($desc.Length -gt 160) { $desc = $desc.Substring(0, 157) + '...' }
[void]$rows.Add([pscustomobject]@{
Dir = $dir
Name = $name
Desc = $desc
Path = $rel
})
}
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# reverse-skill 技能导航索引')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('> 本文件由 `skills/scripts/extract-summaries.ps1` 自动生成,**请勿手改**。')
[void]$sb.AppendLine('> 修改摘要请编辑对应模块 `SKILL.md` 的 frontmatter `description`,然后重跑脚本。')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 模块总览')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| 模块 | 摘要 |')
[void]$sb.AppendLine('|------|------|')
foreach ($r in $rows) {
$esc = $r.Desc -replace '\|', '\|'
[void]$sb.AppendLine(("| [{0}]({1}) | {2} |" -f $r.Name, ($r.Path -replace '\\', '/'), $esc))
}
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 目录树')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('```')
foreach ($r in $rows) {
[void]$sb.AppendLine(("skills/{0}/" -f ($r.Path -replace '\\', '/')))
}
[void]$sb.AppendLine('```')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 路由')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('PRIMARY 路由由 `skills/config/routing.json`(唯一事实源)驱动,用 `master-route.ps1 -Hint "<任务>"` 分诊。')
[void]$sb.AppendLine('歧义场景读 `skills/routing.md` 全矩阵;CTF 多类型任务走 `CTF-Sandbox-Orchestrator/`。')
$newContent = $sb.ToString()
# 统一 LF 行尾:与 .gitattributes (*.md eol=lf) 保持一致,保证 clone 后 -Check 幂等
$newContent = $newContent -replace "`r`n", "`n"
$utf8 = New-Object System.Text.UTF8Encoding $true
if ($Check) {
if (-not (Test-Path -LiteralPath $indexPath)) {
Write-Host '[CHECK] INDEX.md missing' -ForegroundColor Red
exit 1
}
# 行尾免疫:autocrlf=true 的机器上 worktree 可能是 CRLF,normalize 后再比
$old = ([System.IO.File]::ReadAllText($indexPath)) -replace "`r`n", "`n"
if ($old -eq $newContent) {
Write-Host '[CHECK] INDEX.md up to date' -ForegroundColor Green
exit 0
}
Write-Host '[CHECK] INDEX.md out of date (run extract-summaries.ps1)' -ForegroundColor Red
exit 1
}
[System.IO.File]::WriteAllText($indexPath, $newContent, $utf8)
Write-Host ("INDEX.md regenerated: {0} modules" -f $rows.Count) -ForegroundColor Green