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

175 lines
7.8 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
# reverse-skill PRIMARY router.
# 规则单一事实源:skills/config/routing.json(勿在本脚本内硬编码路由表)。
# CLI 兼容旧版:-Hint / -OutDir;输出 route-scope.md;退出码 0 成功 / 2 配置或技能缺失。
# 读取 UTF-8 BOM 源以保证 Windows PowerShell 5.1 下 CJK 正常。
param(
[string] $Hint = '',
[string] $OutDir = '',
[string] $ProjectRoot = ''
)
$ErrorActionPreference = 'Stop'
# Lowercase for Latin tokens; CJK unchanged by ToLowerInvariant.
$t = if ($Hint) { $Hint.ToLowerInvariant() } else { '' }
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
$configPath = Join-Path $skillsRoot 'config\routing.json'
# --- 读取路由配置(单一事实源) ---
if (-not (Test-Path -LiteralPath $configPath)) {
Write-Host ("ERROR: routing config missing: {0}" -f $configPath) -ForegroundColor Red
Write-Host 'Restore skills/config/routing.json (git checkout / git pull) and retry.' -ForegroundColor Yellow
exit 2
}
try {
$cfg = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
} catch {
Write-Host ("ERROR: routing config is not valid JSON: {0}" -f $_.Exception.Message) -ForegroundColor Red
exit 2
}
# --- 规则匹配:每条 keyword 规则命中则计入候选集(与旧版逐条 if 行为一致) ---
$sel = New-Object System.Collections.Generic.List[string]
foreach ($route in $cfg.routes.PSObject.Properties) {
$id = $route.Name
foreach ($kw in $route.Value.keywords) {
$hit = $false
if ($null -ne $kw.must -and $t -match $kw.must) { $hit = $true }
# mustAll:全部子正则都匹配才成立
if ($hit -and $null -ne $kw.mustAll) {
foreach ($m in $kw.mustAll) {
if ($t -notmatch $m) { $hit = $false; break }
}
}
# exclude:命中则本条规则不触发(防误伤,如 越狱/LLM 语境、域控/攻击链)
if ($hit -and $null -ne $kw.exclude -and $t -match $kw.exclude) { $hit = $false }
if ($hit) { [void]$sel.Add($id) }
}
}
# --- 计分:同 id 多规则命中则加分(与旧版一致) ---
$scores = [ordered]@{}
foreach ($item in $sel) {
if (-not $scores.Contains($item)) { $scores[$item] = 0 }
$scores[$item] = $scores[$item] + 1
}
$uniq = New-Object System.Collections.Generic.List[string]
foreach ($d in $scores.Keys) { [void]$uniq.Add($d) }
# --- priority 自检:路由表与 priority 必须一一对应,防新增路由漏加优先级 ---
$routeIds = @($cfg.routes.PSObject.Properties | ForEach-Object { $_.Name })
$missingInPriority = @($routeIds | Where-Object { $_ -notin @($cfg.priority) })
$extraInPriority = @($cfg.priority | Where-Object { $_ -notin $routeIds })
if ($missingInPriority.Count -gt 0 -or $extraInPriority.Count -gt 0) {
Write-Host 'WARN: routing.json routes/priority mismatch:' -ForegroundColor Yellow
if ($missingInPriority.Count -gt 0) { Write-Host (" routes not in priority: {0}" -f ($missingInPriority -join ', ')) -ForegroundColor Yellow }
if ($extraInPriority.Count -gt 0) { Write-Host (" priority not in routes: {0}" -f ($extraInPriority -join ', ')) -ForegroundColor Yellow }
}
# --- 按 priority 顺序取分数最高者为 PRIMARY(并列时 priority 靠前者胜出) ---
$priority = @($cfg.priority)
$fallbackId = $cfg.meta.fallbackId
$primary = $null
$maxScore = -1
foreach ($p in $priority) {
if ($scores.Contains($p)) {
$score = $scores[$p]
if ($score -gt $maxScore) {
$maxScore = $score
$primary = $p
}
}
}
$notes = New-Object System.Collections.Generic.List[string]
$confidence = 'low'
if ($null -eq $primary) {
$primary = $fallbackId
if ($t) { [void]$notes.Add('No strong keyword hit; open routing.md full matrix') }
else { [void]$notes.Add('Empty hint; provide task text') }
} else {
$confidence = if ($uniq.Count -eq 1) { 'high' } else { 'medium' }
}
# 防御:priority 若含 routes 之外的幽灵 id(配置错误),回退到 fallback 而非崩溃
if ($routeIds -notcontains $primary) {
Write-Host ("WARN: primary id '{0}' not in routes; falling back to {1}" -f $primary, $fallbackId) -ForegroundColor Yellow
$primary = $fallbackId
}
# 复用当前主线的项目工作根解析,避免把路由产物写进 skill 包。
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
. (Join-Path (Join-Path $scriptDir 'lib') 'WorkRoot.ps1')
$projectRoot = Resolve-ReverseProjectRoot -RequestedRoot $ProjectRoot
if ([string]::IsNullOrWhiteSpace($OutDir)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$workRoot = Join-Path $projectRoot 'work'
$OutDir = Join-Path $workRoot ("master-route-{0}" -f $stamp)
}
$primaryPath = $cfg.routes.$primary.skill
$primaryLabel = $cfg.routes.$primary.label
$skillAbs = Join-Path $skillsRoot ($primaryPath -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path -LiteralPath $skillAbs)) {
Write-Host ("ERROR: PRIMARY skill missing: {0}" -f $skillAbs) -ForegroundColor Red
exit 2
}
# --- 输出目录(默认调用方项目 work\master-route-<ts>) ---
if ([string]::IsNullOrWhiteSpace($OutDir)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
if ($packageRoot -and (Test-Path -LiteralPath $packageRoot)) {
$OutDir = Join-Path (Join-Path $packageRoot 'work') ("master-route-{0}" -f $stamp)
} else {
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
$OutDir = Join-Path (Join-Path $tmpBase 'reverse-skill-route') ("master-route-{0}" -f $stamp)
}
}
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
# --- 写 route-scope.md(格式与旧版一致,下游脚本解析 primary_skill / primary) ---
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# reverse-skill Master route (PRIMARY)')
[void]$sb.AppendLine(("- created: {0}" -f (Get-Date -Format 'o')))
[void]$sb.AppendLine(("- package: reverse-skill"))
[void]$sb.AppendLine(("- hint: {0}" -f $Hint))
[void]$sb.AppendLine(("- primary: {0}" -f $primary))
[void]$sb.AppendLine(("- primary_label: {0}" -f $primaryLabel))
[void]$sb.AppendLine(("- primary_skill: skills/{0}" -f $primaryPath))
[void]$sb.AppendLine(("- confidence: {0}" -f $confidence))
[void]$sb.AppendLine(("- project_root: {0}" -f $projectRoot))
$sec = New-Object System.Collections.Generic.List[string]
foreach ($d in $uniq) {
if ($d -ne $primary) { [void]$sec.Add(("skills/{0}" -f $cfg.routes.$d.skill)) }
}
$secText = if ($sec.Count -gt 0) { ($sec -join ', ') } else { '(none)' }
[void]$sb.AppendLine(("- secondary: {0}" -f $secText))
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## MUST open next')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('1. skills/MASTER-ROUTING.md')
[void]$sb.AppendLine(("2. skills/{0}" -f $primaryPath))
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## Notes')
if ($notes.Count -eq 0) { [void]$sb.AppendLine('- (none)') }
foreach ($n in $notes) { [void]$sb.AppendLine(("- {0}" -f $n)) }
# UTF-8 with BOM for route-scope (Windows notepad-friendly)
$utf8 = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText((Join-Path $OutDir 'route-scope.md'), $sb.ToString(), $utf8)
Write-Host ("PRIMARY -> skills/{0}" -f $primaryPath) -ForegroundColor Green
Write-Host ("Label: {0} | confidence: {1}" -f $primaryLabel, $confidence)
foreach ($n in $notes) { Write-Host ("NOTE: {0}" -f $n) -ForegroundColor Yellow }
Write-Host ("Wrote {0}\route-scope.md" -f $OutDir)
Write-Host 'ACTION: Open PRIMARY SKILL.md now and execute ACTION REQUIRED.' -ForegroundColor Yellow