- clash.ps1: 1:1 port of macOS clash script to PowerShell 7
- TUN-only mode, auto UAC elevation via Start-Process -Verb RunAs
- Subcommands: start/stop/restart/reload/status/link/open/edit/
log/cleanup/doctor/verge-off/help + interactive menu
- win_adapt_config: strip Linux-only fields, stack system→gvisor
- wintun adapter + route cleanup for network recovery
- ANSI colors (pwsh 7 + Windows Terminal)
- 5-path mihomo binary detection (Get-Command→scoop→~/Mihomo→LOCALAPPDATA→ProgramFiles)
- install.ps1: one-command Windows deploy
- Detect pwsh 7, download mihomo v1.19.15 + wintun.dll 0.14.1
- Architecture-aware (amd64/arm64)
- Deploy clash.ps1 to PATH
- README.md: add Windows section
- .gitignore: add wintun.dll, *.exe
757 lines
26 KiB
PowerShell
757 lines
26 KiB
PowerShell
#!/usr/bin/env pwsh
|
||
# ═══════════════════════════════════════════════════════════
|
||
# Mihomo Kernel Manager — Windows
|
||
# clash.ps1 <command> | clash.ps1 (交互菜单) | clash.ps1 help
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
# ─── 路径变量 ───
|
||
$ConfigDir = "$env:USERPROFILE\.config\mihomo"
|
||
$ConfigFile = "$ConfigDir\config.yaml"
|
||
$PidFile = "$ConfigDir\mihomo.pid"
|
||
$LogFile = "$ConfigDir\mihomo.log"
|
||
$ErrLogFile = "$ConfigDir\mihomo.err.log"
|
||
$LinkFile = "$ConfigDir\source.txt"
|
||
$SourceDir = "$env:USERPROFILE\Mihomo"
|
||
$MixedPort = 7890
|
||
$ApiPort = 9090
|
||
|
||
if (-not (Test-Path $ConfigDir)) { New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null }
|
||
|
||
# ─── 颜色 ───
|
||
$e = [char]27
|
||
$Bold = "$e[1m"; $Dim = "$e[2m"
|
||
$Green = "$e[0;32m"; $BoldGreen = "$e[1;32m"
|
||
$Red = "$e[0;31m"; $BoldRed = "$e[1;31m"
|
||
$Yellow = "$e[0;33m"; $BoldYellow = "$e[1;33m"
|
||
$Blue = "$e[0;34m"; $BoldBlue = "$e[1;34m"
|
||
$Purple = "$e[0;35m"; $BoldPurple = "$e[1;35m"
|
||
$Cyan = "$e[0;36m"; $BoldCyan = "$e[1;36m"
|
||
$NC = "$e[0m"
|
||
$SEP = "$Dim$('─' * 42)$NC"
|
||
|
||
# ─── 输出辅助 ───
|
||
function W-Info { Write-Host " ${Cyan}ℹ${NC} $args" }
|
||
function W-Ok { Write-Host " ${Green}✓${NC} $args" }
|
||
function W-Warn { Write-Host " ${Yellow}⚠${NC} $args" }
|
||
function W-Err { Write-Host " ${Red}✗${NC} $args" }
|
||
function W-Dim { Write-Host " ${Dim}$args${NC}" }
|
||
function W-Section { Write-Host ""; Write-Host " ${Bold}── $args ──${NC}" }
|
||
|
||
# ─── mihomo 二进制查找 ───
|
||
function Get-MihomoBin {
|
||
$cmd = Get-Command mihomo -ErrorAction SilentlyContinue
|
||
if ($cmd) { return $cmd.Source }
|
||
$paths = @(
|
||
"$env:USERPROFILE\scoop\apps\mihomo\current\mihomo.exe",
|
||
"$env:USERPROFILE\Mihomo\mihomo.exe",
|
||
"$env:LOCALAPPDATA\Programs\mihomo\mihomo.exe",
|
||
"$env:ProgramFiles\mihomo\mihomo.exe"
|
||
)
|
||
foreach ($p in $paths) { if (Test-Path $p) { return $p } }
|
||
return $null
|
||
}
|
||
|
||
$Script:MihomoBin = Get-MihomoBin
|
||
|
||
# ─── wintun.dll 检测 ───
|
||
function Test-Wintun {
|
||
if (-not $Script:MihomoBin) { return $false }
|
||
$dir = Split-Path $Script:MihomoBin -Parent
|
||
return (Test-Path (Join-Path $dir "wintun.dll"))
|
||
}
|
||
|
||
# ─── 管理员检测 ───
|
||
function Test-Admin {
|
||
$current = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||
(New-Object Security.Principal.WindowsPrincipal($current)).IsInRole(
|
||
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
}
|
||
|
||
# ─── 运行状态 ───
|
||
function Test-Running {
|
||
if (-not (Test-Path $PidFile)) { return $false }
|
||
$procId = [System.IO.File]::ReadAllText($PidFile).Trim()
|
||
if (-not $procId) { return $false }
|
||
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
||
return ($null -ne $proc)
|
||
}
|
||
|
||
# ─── 源文件自动检测 ───
|
||
function Get-AutoDetectSource {
|
||
if (Test-Path $LinkFile) {
|
||
$linked = [System.IO.File]::ReadAllText($LinkFile).Trim()
|
||
if (Test-Path $linked) { return $linked }
|
||
W-Warn "链接文件不存在: $linked,尝试自动检测..."
|
||
}
|
||
if (Test-Path $SourceDir) {
|
||
$latest = Get-ChildItem -Path $SourceDir -File |
|
||
Where-Object { $_.Extension -in '.yaml', '.yml' } |
|
||
Sort-Object LastWriteTime -Descending |
|
||
Select-Object -First 1
|
||
if ($latest) { return $latest.FullName }
|
||
}
|
||
return $null
|
||
}
|
||
|
||
# ─── Windows 配置适配(不改源文件)───
|
||
function Invoke-WinAdaptConfig {
|
||
param([string]$Src)
|
||
|
||
if (-not $Src) { $Src = Get-AutoDetectSource }
|
||
if (-not $Src -or -not (Test-Path $Src)) {
|
||
W-Err "未找到 YAML 配置文件"
|
||
W-Dim "请把 .yaml 放到 $SourceDir,或用: clash.ps1 link C:\path\to\file.yaml"
|
||
return $false
|
||
}
|
||
|
||
Write-Host -NoNewline " 适配 Windows 配置 ... "
|
||
Copy-Item $Src $ConfigFile -Force
|
||
$content = [System.IO.File]::ReadAllText($ConfigFile)
|
||
|
||
# Windows 兼容修复(与 macOS 版逻辑一致)
|
||
$content = $content -replace '(?m)^\s*device:\s*nikki\s*\r?\n', ''
|
||
$content = $content -replace '(?m)^\s*auto-redirect:\s*true\s*\r?\n', ''
|
||
$content = $content -replace '(?m)^(\s*stack:)\s*system', '$1 gvisor'
|
||
$content = $content -replace ' - tcp://', ' - '
|
||
$content = $content -replace ' - udp://', ' - '
|
||
|
||
[System.IO.File]::WriteAllText($ConfigFile, $content, [System.Text.UTF8Encoding]::new($false))
|
||
|
||
Write-Host "${Green}✓${NC}"
|
||
W-Dim "源文件 $Src"
|
||
W-Dim "适配后 $ConfigFile"
|
||
return $true
|
||
}
|
||
|
||
# ─── 管理面板 ───
|
||
function Get-UiUrl {
|
||
$uiDir = "$ConfigDir\ui"
|
||
if (Test-Path $uiDir) {
|
||
$dash = Get-ChildItem $uiDir -Directory | Sort-Object Name | Select-Object -First 1
|
||
if ($dash) { return "http://127.0.0.1:$ApiPort/ui/$($dash.Name)/" }
|
||
}
|
||
return "http://127.0.0.1:$ApiPort/ui"
|
||
}
|
||
|
||
function Update-UiIndex {
|
||
$uiDir = "$ConfigDir\ui"
|
||
if (-not (Test-Path $uiDir)) { return }
|
||
$dash = Get-ChildItem $uiDir -Directory | Sort-Object Name | Select-Object -First 1
|
||
if (-not $dash) { return }
|
||
$indexFile = "$uiDir\index.html"
|
||
if (-not (Test-Path $indexFile)) {
|
||
$html = @"
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="refresh" content="0; url=./$($dash.Name)/">
|
||
<title>Mihomo Dashboard</title>
|
||
</head>
|
||
<body>
|
||
<p>正在打开面板 <a href="./$($dash.Name)/">$($dash.Name)</a> ...</p>
|
||
</body>
|
||
</html>
|
||
"@
|
||
[System.IO.File]::WriteAllText($indexFile, $html, [System.Text.UTF8Encoding]::new($false))
|
||
W-Info "已生成面板首页跳转: $indexFile"
|
||
}
|
||
}
|
||
|
||
# ─── 残留网络状态检测 ───
|
||
function Test-NetworkLeftovers {
|
||
# 1. 残留 mihomo 进程
|
||
if (Get-Process -Name mihomo -ErrorAction SilentlyContinue) { return $true }
|
||
|
||
# 2. 残留 wintun 网卡
|
||
$adapters = Get-NetAdapter -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" }
|
||
if ($adapters) { return $true }
|
||
|
||
# 3. 残留路由(指向 198.18.0.1)
|
||
$routes = Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -eq "198.18.0.1" }
|
||
if ($routes) { return $true }
|
||
|
||
# 4. DNS 劫持(指向 127.0.0.1 或 198.18.x)
|
||
$dns = Get-DnsClientServerAddress -ErrorAction SilentlyContinue | Where-Object {
|
||
$_.ServerAddresses -contains "127.0.0.1" -or
|
||
($_.ServerAddresses | Where-Object { $_ -match "^198\.18\." })
|
||
}
|
||
if ($dns) { return $true }
|
||
|
||
return $false
|
||
}
|
||
|
||
# ─── 网络恢复(TUN/路由/DNS 全面清理)───
|
||
function Invoke-NetworkRestore {
|
||
param([bool]$ShowProgress = $true)
|
||
|
||
if ($ShowProgress) { W-Section "网络恢复" }
|
||
|
||
# 1. 清理残留 mihomo 进程
|
||
$procs = Get-Process -Name mihomo -ErrorAction SilentlyContinue
|
||
if ($procs) {
|
||
if ($ShowProgress) { W-Warn "发现残留进程,正在清理..." }
|
||
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
# 2. 清理残留 wintun 网卡
|
||
$adapters = Get-NetAdapter -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" }
|
||
foreach ($adapter in $adapters) {
|
||
if ($ShowProgress) { Write-Host -NoNewline " 清理残留 TUN $($adapter.Name) ... " }
|
||
netsh interface set interface "$($adapter.Name)" admin=disable 2>$null | Out-Null
|
||
if ($ShowProgress) { Write-Host "${Green}✓${NC}" }
|
||
}
|
||
|
||
# 3. 清理残留路由
|
||
$mihomoRoutes = Get-NetRoute -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.NextHop -eq "198.18.0.1" }
|
||
foreach ($route in $mihomoRoutes) {
|
||
Remove-NetRoute -DestinationPrefix $route.DestinationPrefix -NextHop "198.18.0.1" -Confirm:$false -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
# 4. 刷新 DNS 缓存
|
||
if ($ShowProgress) { Write-Host -NoNewline " 刷新 DNS 缓存 ... " }
|
||
ipconfig /flushdns 2>$null | Out-Null
|
||
if ($ShowProgress) { Write-Host "${Green}✓${NC}" }
|
||
|
||
# 5. 还原系统 DNS(如果被 mihomo 指到 127.0.0.1 / 198.18.x)
|
||
$dnsHijacked = Get-DnsClientServerAddress -ErrorAction SilentlyContinue | Where-Object {
|
||
$_.ServerAddresses -contains "127.0.0.1" -or
|
||
($_.ServerAddresses | Where-Object { $_ -match "^198\.18\." })
|
||
}
|
||
if ($dnsHijacked) {
|
||
if ($ShowProgress) { Write-Host -NoNewline " 还原系统 DNS ... " }
|
||
foreach ($adapter in $dnsHijacked) {
|
||
Set-DnsClientServerAddress -InterfaceIndex $adapter.InterfaceIndex -ResetServerAddresses -ErrorAction SilentlyContinue
|
||
}
|
||
ipconfig /flushdns 2>$null | Out-Null
|
||
if ($ShowProgress) { Write-Host "${Green}✓${NC}" }
|
||
}
|
||
|
||
if ($ShowProgress) {
|
||
Write-Host ""
|
||
W-Ok "网络已恢复"
|
||
}
|
||
}
|
||
|
||
# ─── 网络诊断 ───
|
||
function Invoke-NetworkHealthCheck {
|
||
$issues = $false
|
||
W-Section "网络诊断"
|
||
|
||
# DNS 解析
|
||
try {
|
||
$null = Resolve-DnsName baidu.com -ErrorAction Stop
|
||
W-Ok "DNS 解析正常 (baidu.com)"
|
||
} catch {
|
||
W-Err "DNS 解析失败"
|
||
$issues = $true
|
||
}
|
||
|
||
# 外网连通
|
||
try {
|
||
$ping = New-Object System.Net.NetworkInformation.Ping
|
||
$reply = $ping.Send("8.8.8.8", 2000)
|
||
if ($reply.Status -eq 'Success') {
|
||
W-Ok "外网连通 (8.8.8.8)"
|
||
} else {
|
||
W-Err "外网不可达"
|
||
$issues = $true
|
||
}
|
||
} catch {
|
||
W-Err "外网不可达"
|
||
$issues = $true
|
||
}
|
||
|
||
# TUN 接口检查
|
||
$wintunAdapters = Get-NetAdapter -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" }
|
||
if (-not $wintunAdapters) {
|
||
W-Ok "无残留 TUN 接口"
|
||
} else {
|
||
W-Warn "发现 $($wintunAdapters.Count) 个残留 TUN 接口"
|
||
$issues = $true
|
||
}
|
||
|
||
if ($issues) {
|
||
W-Warn "网络可能有异常,试试: clash.ps1 cleanup"
|
||
} else {
|
||
W-Ok "网络一切正常"
|
||
}
|
||
}
|
||
|
||
# ─── 启动 ───
|
||
function Invoke-CmdStart {
|
||
param([string]$Src)
|
||
|
||
$doAdapt = $true
|
||
if ($Src) {
|
||
$linked = if (Test-Path $LinkFile) { [System.IO.File]::ReadAllText($LinkFile).Trim() } else { "" }
|
||
if ($Src -eq $ConfigFile -or $Src -eq $linked) { $doAdapt = $false }
|
||
}
|
||
if (-not $Src) { $Src = Get-AutoDetectSource }
|
||
|
||
if ($doAdapt -and $Src -ne $ConfigFile) {
|
||
$result = Invoke-WinAdaptConfig $Src
|
||
if (-not $result) { return }
|
||
} elseif (-not (Test-Path $ConfigFile)) {
|
||
W-Err "配置文件不存在: $ConfigFile"
|
||
W-Dim "试试: clash.ps1 link C:\path\to\your.yaml"
|
||
return
|
||
}
|
||
|
||
if (Test-Running) {
|
||
W-Warn "mihomo 已经在运行了"
|
||
return
|
||
}
|
||
|
||
# 检查 mihomo 二进制
|
||
if (-not $Script:MihomoBin) {
|
||
W-Err "未找到 mihomo.exe"
|
||
W-Dim "请先运行 install.ps1 安装,或手动下载 mihomo.exe"
|
||
return
|
||
}
|
||
|
||
# 检查 wintun.dll
|
||
if (-not (Test-Wintun)) {
|
||
W-Warn "未找到 wintun.dll(TUN 模式需要)"
|
||
W-Dim "请将 wintun.dll 放在 mihomo.exe 同目录,或运行 install.ps1"
|
||
return
|
||
}
|
||
|
||
# 启动前清理残留
|
||
if (Test-NetworkLeftovers) {
|
||
Write-Host ""
|
||
W-Info "检测到上次残留网络状态,先清理 ..."
|
||
Invoke-NetworkRestore $false
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host -NoNewline " 启动中 ... "
|
||
|
||
# 清理旧日志
|
||
Remove-Item $LogFile -Force -ErrorAction SilentlyContinue
|
||
Remove-Item $ErrLogFile -Force -ErrorAction SilentlyContinue
|
||
|
||
# 启动 mihomo(隐藏窗口,重定向输出)
|
||
$argStr = "-d `"$ConfigDir`" -f `"$ConfigFile`""
|
||
$proc = Start-Process -FilePath $Script:MihomoBin `
|
||
-ArgumentList $argStr `
|
||
-WindowStyle Hidden `
|
||
-RedirectStandardOutput $LogFile `
|
||
-RedirectStandardError $ErrLogFile `
|
||
-PassThru
|
||
|
||
[System.IO.File]::WriteAllText($PidFile, $proc.Id.ToString())
|
||
Start-Sleep -Seconds 1
|
||
|
||
if (Test-Running) {
|
||
Write-Host "${Green}✓${NC} 已就绪"
|
||
Update-UiIndex
|
||
Write-Host ""
|
||
Write-Host " ${Dim}代理端口 ${NC}${Cyan}127.0.0.1:$MixedPort${NC} ${Dim}(HTTP/SOCKS5)${NC}"
|
||
Write-Host " ${Dim}API 端口 ${NC}${Cyan}127.0.0.1:$ApiPort${NC}"
|
||
Write-Host " ${Dim}管理面板 ${NC}${Cyan}$(Get-UiUrl)${NC}"
|
||
Write-Host ""
|
||
} else {
|
||
W-Err "启动失败"
|
||
W-Dim "查看日志: Get-Content $LogFile"
|
||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
# ─── 停止 ───
|
||
function Invoke-CmdStop {
|
||
if (-not (Test-Running)) {
|
||
W-Warn "mihomo 没有在运行"
|
||
if (Test-NetworkLeftovers) {
|
||
Write-Host ""
|
||
W-Info "检测到残留网络状态,正在清理 ..."
|
||
Invoke-NetworkRestore $true
|
||
Write-Host ""
|
||
Invoke-NetworkHealthCheck
|
||
}
|
||
return
|
||
}
|
||
|
||
$procId = [int][System.IO.File]::ReadAllText($PidFile).Trim()
|
||
Write-Host ""
|
||
Write-Host -NoNewline " 停止中 ... "
|
||
|
||
Stop-Process -Id $procId -ErrorAction SilentlyContinue
|
||
|
||
for ($i = 0; $i -lt 10; $i++) {
|
||
Start-Sleep -Milliseconds 500
|
||
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
||
if (-not $proc) {
|
||
Write-Host "${Green}✓${NC} 已停止 (PID: $procId)"
|
||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||
Write-Host ""
|
||
Invoke-NetworkRestore $false
|
||
Invoke-NetworkHealthCheck
|
||
return
|
||
}
|
||
}
|
||
|
||
Write-Host ""
|
||
W-Warn "超时,强制终止..."
|
||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
||
Start-Sleep -Milliseconds 500
|
||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||
|
||
if (Get-Process -Name mihomo -ErrorAction SilentlyContinue) {
|
||
W-Err "停止失败,残留进程仍在运行"
|
||
W-Dim "试试: Stop-Process -Name mihomo -Force"
|
||
return
|
||
}
|
||
|
||
W-Warn "已强制停止"
|
||
W-Info "正在恢复网络 ..."
|
||
Invoke-NetworkRestore $true
|
||
Write-Host ""
|
||
Invoke-NetworkHealthCheck
|
||
}
|
||
|
||
# ─── 重启 / 热重载 ───
|
||
function Invoke-CmdRestart {
|
||
param([string]$Src)
|
||
Invoke-CmdStop
|
||
Start-Sleep -Seconds 1
|
||
Invoke-CmdStart $Src
|
||
}
|
||
|
||
function Invoke-CmdReload {
|
||
param([string]$Cfg = $ConfigFile)
|
||
|
||
if (-not (Test-Running)) {
|
||
W-Warn "mihomo 没在运行,直接启动"
|
||
Invoke-CmdStart $Cfg
|
||
return
|
||
}
|
||
if (-not (Test-Path $Cfg)) {
|
||
W-Err "配置文件不存在: $Cfg"
|
||
return
|
||
}
|
||
|
||
Write-Host -NoNewline " 热重载配置 ... "
|
||
try {
|
||
$body = @{ path = $Cfg } | ConvertTo-Json -Compress
|
||
$uri = "http://127.0.0.1:$ApiPort/configs"
|
||
$resp = Invoke-WebRequest -Method PUT -Uri $uri -ContentType "application/json" -Body $body -UseBasicParsing -ErrorAction Stop
|
||
W-Ok "成功"
|
||
} catch {
|
||
W-Err "API 不可达,尝试重启..."
|
||
Invoke-CmdRestart $Cfg
|
||
}
|
||
}
|
||
|
||
# ─── 状态 ───
|
||
function Invoke-CmdStatus {
|
||
if (Test-Running) {
|
||
$procId = [System.IO.File]::ReadAllText($PidFile).Trim()
|
||
Write-Host ""
|
||
Write-Host " ${BoldGreen}● 运行中${NC} ${Dim}PID: $procId${NC}"
|
||
Write-Host " $SEP"
|
||
Write-Host " ${Dim}代理端口 ${NC}${Cyan}127.0.0.1:$MixedPort${NC} (HTTP/SOCKS5)"
|
||
Write-Host " ${Dim}API 端口 ${NC}${Cyan}127.0.0.1:$ApiPort${NC}"
|
||
Write-Host " ${Dim}管理面板 ${NC}${Cyan}$(Get-UiUrl)${NC}"
|
||
Write-Host " ${Dim}配置文件 ${NC}$ConfigFile"
|
||
Write-Host " ${Dim}日志文件 ${NC}$LogFile"
|
||
$src = if (Test-Path $LinkFile) { [System.IO.File]::ReadAllText($LinkFile).Trim() } else { Get-AutoDetectSource }
|
||
if ($src) { Write-Host " ${Dim}源文件 ${NC}$src" }
|
||
Write-Host ""
|
||
} else {
|
||
Write-Host ""
|
||
Write-Host " ${BoldRed}○ 已停止${NC}"
|
||
Write-Host " $SEP"
|
||
W-Dim "mihomo 当前未运行"
|
||
Write-Host ""
|
||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
# ─── 链接源配置 ───
|
||
function Invoke-CmdLink {
|
||
param([string]$Src)
|
||
|
||
if (-not $Src) {
|
||
Write-Host ""
|
||
W-Section "源文件链接"
|
||
if (Test-Path $LinkFile) {
|
||
W-Info "当前链接: $([System.IO.File]::ReadAllText($LinkFile).Trim())"
|
||
} else {
|
||
$detected = Get-AutoDetectSource
|
||
if ($detected) {
|
||
W-Info "自动检测: $detected"
|
||
} else {
|
||
W-Warn "未检测到源文件"
|
||
}
|
||
}
|
||
Write-Host ""
|
||
$input = Read-Host " 请输入 YAML 文件路径 (Enter 取消)"
|
||
if (-not $input) { W-Dim "已取消"; return }
|
||
$Src = $input.Trim().Trim('"')
|
||
}
|
||
|
||
if (-not (Test-Path $Src)) {
|
||
W-Err "文件不存在: $Src"
|
||
return
|
||
}
|
||
|
||
[System.IO.File]::WriteAllText($LinkFile, $Src, [System.Text.UTF8Encoding]::new($false))
|
||
W-Ok "已链接"
|
||
W-Dim "源文件 $Src"
|
||
Write-Host ""
|
||
$ans = Read-Host " 立即适配并启动? (y/n)"
|
||
if ($ans -eq 'y') {
|
||
$result = Invoke-WinAdaptConfig $Src
|
||
if (-not $result) { return }
|
||
if (Test-Running) {
|
||
Invoke-CmdRestart
|
||
} else {
|
||
Invoke-CmdStart
|
||
}
|
||
}
|
||
}
|
||
|
||
# ─── 日志 / 编辑 / 面板 / 修复 / 诊断 ───
|
||
function Invoke-CmdLog {
|
||
if (Test-Path $LogFile) {
|
||
Write-Host ""
|
||
W-Section "实时日志 (Ctrl+C 退出)"
|
||
Get-Content $LogFile -Wait
|
||
} else {
|
||
W-Warn "日志文件不存在"
|
||
}
|
||
}
|
||
|
||
function Invoke-CmdEdit {
|
||
W-Info "打开编辑器: $ConfigFile"
|
||
$editor = $env:EDITOR
|
||
if (-not $editor) { $editor = "notepad" }
|
||
Start-Process $editor -ArgumentList "`"$ConfigFile`"" -Wait
|
||
}
|
||
|
||
function Invoke-CmdOpen {
|
||
if (Test-Running) {
|
||
Update-UiIndex
|
||
$url = Get-UiUrl
|
||
Start-Process $url
|
||
W-Ok "已打开管理面板"
|
||
W-Dim $url
|
||
} else {
|
||
W-Warn "mihomo 未运行,请先启动"
|
||
}
|
||
}
|
||
|
||
function Invoke-CmdCleanup {
|
||
Invoke-NetworkRestore $true
|
||
}
|
||
|
||
function Invoke-CmdDoctor {
|
||
Invoke-NetworkHealthCheck
|
||
}
|
||
|
||
# ─── 关闭 Clash Verge 后台服务 ───
|
||
function Invoke-CmdVergeOff {
|
||
Write-Host ""
|
||
W-Section "关闭 Clash Verge"
|
||
|
||
$svc = Get-Service -ErrorAction SilentlyContinue | Where-Object {
|
||
$_.Name -like "*clash*verge*" -or $_.Name -like "*clash_verge*"
|
||
}
|
||
|
||
if ($svc) {
|
||
W-Info "发现 Clash Verge 服务: $($svc.Name) ($($svc.Status))"
|
||
Write-Host -NoNewline " 正在停止 ... "
|
||
if ($svc.Status -eq 'Running') {
|
||
Stop-Service -Name $svc.Name -Force -ErrorAction SilentlyContinue
|
||
}
|
||
Start-Sleep -Seconds 1
|
||
# 杀残留进程
|
||
Get-Process -Name "clash-verge*", "clash_verge*" -ErrorAction SilentlyContinue |
|
||
Stop-Process -Force -ErrorAction SilentlyContinue
|
||
W-Ok "已停止"
|
||
} else {
|
||
$procs = Get-Process -Name "clash-verge*", "clash_verge*" -ErrorAction SilentlyContinue
|
||
if ($procs) {
|
||
W-Info "发现 Clash Verge 进程"
|
||
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||
W-Ok "已停止"
|
||
} else {
|
||
W-Ok "Clash Verge 服务未运行"
|
||
}
|
||
}
|
||
|
||
Write-Host ""
|
||
$ans = Read-Host " 是否同时卸载开机自启? (y/n)"
|
||
if ($ans -eq 'y') {
|
||
if ($svc) {
|
||
sc.exe delete $svc.Name 2>$null | Out-Null
|
||
W-Ok "已卸载开机自启"
|
||
} else {
|
||
W-Dim "未找到注册服务,跳过"
|
||
}
|
||
}
|
||
Write-Host ""
|
||
W-Ok "现在可以纯用 clash.ps1 管理了"
|
||
Write-Host ""
|
||
}
|
||
|
||
# ─── 帮助 ───
|
||
function Show-Help {
|
||
Write-Host ""
|
||
Write-Host " ${BoldPurple}Mihomo Kernel Manager${NC} ${Dim}— Windows${NC}"
|
||
Write-Host " $SEP"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}用法:${NC}"
|
||
Write-Host " ${Cyan}clash.ps1 <command>${NC} 执行子命令"
|
||
Write-Host " ${Cyan}clash.ps1${NC} 进入交互菜单"
|
||
Write-Host " ${Cyan}clash.ps1 help${NC} 显示本帮助"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}核心:${NC}"
|
||
Write-Host " ${Cyan}start [file]${NC} 启动(自动适配 Windows 配置,TUN 模式)"
|
||
Write-Host " ${Cyan}stop${NC} 停止并恢复网络"
|
||
Write-Host " ${Cyan}restart${NC} 重启"
|
||
Write-Host " ${Cyan}status${NC} 查看运行状态"
|
||
Write-Host " ${Cyan}reload [file]${NC} 热重载配置"
|
||
Write-Host " ${Cyan}open${NC} 打开管理面板"
|
||
Write-Host " ${Cyan}log${NC} 实时日志"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}配置:${NC}"
|
||
Write-Host " ${Cyan}link [file]${NC} 查看/设置源 YAML"
|
||
Write-Host " ${Cyan}edit${NC} 编辑当前配置"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}网络:${NC}"
|
||
Write-Host " ${Cyan}cleanup${NC} 网络修复(清理残留 TUN/路由/DNS)"
|
||
Write-Host " ${Cyan}doctor${NC} 网络诊断"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}系统:${NC}"
|
||
Write-Host " ${Cyan}verge-off${NC} 关闭 Clash Verge 后台服务"
|
||
Write-Host ""
|
||
Write-Host " ${Bold}示例:${NC}"
|
||
W-Dim "clash.ps1 start"
|
||
W-Dim 'clash.ps1 link C:\Users\you\Mihomo\config.yaml'
|
||
W-Dim "clash.ps1 cleanup"
|
||
Write-Host ""
|
||
}
|
||
|
||
# ─── 交互菜单 ───
|
||
function Show-Menu {
|
||
Clear-Host
|
||
Write-Host ""
|
||
Write-Host " ${BoldPurple}Mihomo Kernel Manager${NC} ${Dim}— Windows${NC}"
|
||
Write-Host " $SEP"
|
||
|
||
if (Test-Running) {
|
||
$procId = [System.IO.File]::ReadAllText($PidFile).Trim()
|
||
Write-Host " ${BoldGreen}● 运行中${NC} ${Dim}PID: $procId 代理: $MixedPort API: $ApiPort${NC}"
|
||
} else {
|
||
Write-Host " ${BoldRed}○ 已停止${NC}"
|
||
}
|
||
Write-Host ""
|
||
|
||
Write-Host " ${Bold}1${NC} 启动 ${Bold}5${NC} 状态"
|
||
Write-Host " ${Bold}2${NC} 停止 ${Bold}6${NC} 实时日志"
|
||
Write-Host " ${Bold}3${NC} 打开面板 ${Bold}7${NC} 热重载"
|
||
Write-Host " ${Bold}4${NC} 重启 ${Bold}8${NC} 网络修复"
|
||
Write-Host " ${Bold}0${NC} 退出"
|
||
Write-Host ""
|
||
Write-Host -NoNewline " ${Bold}❯${NC} "
|
||
}
|
||
|
||
function Invoke-InteractiveMenu {
|
||
while ($true) {
|
||
Show-Menu
|
||
$choice = Read-Host
|
||
Write-Host ""
|
||
switch ($choice) {
|
||
'1' { Invoke-CmdStart }
|
||
'2' { Invoke-CmdStop }
|
||
'3' { Invoke-CmdOpen }
|
||
'4' { Invoke-CmdRestart }
|
||
'5' { Invoke-CmdStatus }
|
||
'6' { Invoke-CmdLog }
|
||
'7' { Invoke-CmdReload }
|
||
'8' { Invoke-CmdCleanup }
|
||
{ $_ -in '0', 'q', 'Q' } {
|
||
Write-Host " ${Dim}再见 👋${NC}"
|
||
Write-Host ""
|
||
return
|
||
}
|
||
default { W-Warn "无效选项,请输入 0-8" }
|
||
}
|
||
Write-Host ""
|
||
Write-Host -NoNewline " 按 Enter 返回菜单..."
|
||
Read-Host
|
||
}
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 入口
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
# 检测是否为提权重启
|
||
$Script:Elevated = $false
|
||
$scriptArgs = $args
|
||
if ($args.Count -gt 0 -and $args[0] -eq '--elevated') {
|
||
$Script:Elevated = $true
|
||
$scriptArgs = if ($args.Count -gt 1) { $args[1..($args.Count-1)] } else { @() }
|
||
}
|
||
|
||
$cmd = if ($scriptArgs.Count -gt 0) { $scriptArgs[0] } else { "" }
|
||
$cmdArgs = if ($scriptArgs.Count -gt 1) { $scriptArgs[1..($scriptArgs.Count-1)] } else { @() }
|
||
|
||
# 需要管理员的命令
|
||
$adminCmds = @('start', 's', 'stop', 'restart', 'r', 'cleanup', 'c', 'verge-off', '')
|
||
|
||
# 自动提权(UAC 关闭时无感)
|
||
if (-not $Script:Elevated -and ($cmd -in $adminCmds) -and -not (Test-Admin)) {
|
||
$elevateArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath, "--elevated")
|
||
if ($cmd) { $elevateArgs += $cmd }
|
||
foreach ($a in $cmdArgs) { $elevateArgs += $a }
|
||
Start-Process "pwsh.exe" -Verb RunAs -ArgumentList $elevateArgs
|
||
exit
|
||
}
|
||
|
||
# 命令分发
|
||
if ($cmd -eq 'start' -or $cmd -eq 's') {
|
||
Invoke-CmdStart ($cmdArgs | Select-Object -First 1)
|
||
} elseif ($cmd -eq 'stop') {
|
||
Invoke-CmdStop
|
||
} elseif ($cmd -eq 'restart' -or $cmd -eq 'r') {
|
||
Invoke-CmdRestart ($cmdArgs | Select-Object -First 1)
|
||
} elseif ($cmd -eq 'reload' -or $cmd -eq 'rl') {
|
||
Invoke-CmdReload ($cmdArgs | Select-Object -First 1)
|
||
} elseif ($cmd -eq 'status' -or $cmd -eq 'st') {
|
||
Invoke-CmdStatus
|
||
} elseif ($cmd -eq 'log' -or $cmd -eq 'l') {
|
||
Invoke-CmdLog
|
||
} elseif ($cmd -eq 'link' -or $cmd -eq 'lk') {
|
||
Invoke-CmdLink ($cmdArgs | Select-Object -First 1)
|
||
} elseif ($cmd -eq 'open' -or $cmd -eq 'o') {
|
||
Invoke-CmdOpen
|
||
} elseif ($cmd -eq 'edit' -or $cmd -eq 'e') {
|
||
Invoke-CmdEdit
|
||
} elseif ($cmd -eq 'cleanup' -or $cmd -eq 'c') {
|
||
Invoke-CmdCleanup
|
||
} elseif ($cmd -eq 'doctor' -or $cmd -eq 'd') {
|
||
Invoke-CmdDoctor
|
||
} elseif ($cmd -eq 'verge-off') {
|
||
Invoke-CmdVergeOff
|
||
} elseif ($cmd -eq 'help' -or $cmd -eq '-h' -or $cmd -eq '--help' -or $cmd -eq 'h') {
|
||
Show-Help
|
||
} else {
|
||
Invoke-InteractiveMenu
|
||
}
|
||
|
||
# 提权模式下非交互命令结束时暂停(让用户看到输出)
|
||
if ($Script:Elevated -and $cmd -ne '' -and $cmd -ne 'help' -and $cmd -ne '-h' -and $cmd -ne '--help' -and $cmd -ne 'h') {
|
||
Write-Host ""
|
||
Write-Host -NoNewline " 按 Enter 关闭..."
|
||
Read-Host
|
||
}
|