<# .SYNOPSIS Sing-box Manager (WinSW Edition) v8.13 Final [Cyberpunk UI] #> param( [switch]$Start, [switch]$Stop, [switch]$Restart, [switch]$Monitor, [int]$MonitorRefreshMs = 1000 ) # ==================== 0. 环境初始化 ==================== $ErrorActionPreference = "SilentlyContinue" $ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Get-Location } Set-Location $ScriptDir # ==================== 1. 核心定义 ==================== $ExeName = "sing-box" $ServiceBase = "singbox-service" $ServiceName = "Sing-box-Service" $ServiceTitle = "Sing-box Core Service" $SingBoxPath = Join-Path $ScriptDir "$ExeName.exe" $ConfigPath = Join-Path $ScriptDir "config.json" $ServiceExe = Join-Path $ScriptDir "$ServiceBase.exe" $ServiceXml = Join-Path $ScriptDir "$ServiceBase.xml" $LogFile = Join-Path $ScriptDir "$ServiceBase.err.log" $PidFile = Join-Path $ScriptDir "service.pid" $ConfigBackupDir = Join-Path $ScriptDir "config_backups" $LogArchiveDir = Join-Path $ScriptDir "log_archives" $ConfigNameFile = Join-Path $ScriptDir ".current_config_name" $WinSWUrl = "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe" $TaskName = "SingBox_Delayed_Start" # ==================== 2. 视觉引擎 (Cyberpunk) ==================== function Write-TrueColor { param([string]$Text, [int]$R, [int]$G, [int]$B, [switch]$NewLine) $Esc = [char]27 $Seq = "$Esc[38;2;$R;$G;${B}m" if ($NewLine) { Write-Host "$Seq$Text$Esc[0m" } else { Write-Host "$Seq$Text$Esc[0m" -NoNewline } } function Draw-Separator { # 霓虹渐变分割线 $Line = "════════════════════════════════════════════════════════════════" Write-TrueColor $Line 80 0 80 -NewLine } function Draw-Gradient-Art { $ArtLines = @( "███████╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗", "██╔════╝██║████╗ ██║██╔════╝ ██╔══██╗██╔═══██╗╚██╗██╔╝", "███████╗██║██╔██╗ ██║██║ ███╗ _____ ██████╔╝██║ ██║ ╚███╔╝ ", "╚════██║██║██║╚██╗██║██║ ██║|_____|██╔══██╗██║ ██║ ██╔██╗ ", "███████║██║██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝██╔╝ ██╗", "╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝" ) $Colors = @( @{R = 255; G = 20; B = 147 }, @{R = 255; G = 0; B = 255 }, @{R = 186; G = 85; B = 211 }, @{R = 138; G = 43; B = 226 }, @{R = 0; G = 191; B = 255 }, @{R = 0; G = 255; B = 255 } ) Write-Host "" for ($i = 0; $i -lt $ArtLines.Count; $i++) { $c = $Colors[$i] Write-TrueColor (" " * 8 + $ArtLines[$i]) $c.R $c.G $c.B -NewLine } Write-Host "" Write-TrueColor " >>> SING-BOX MANAGER v8.13 <<<" 255 215 0 -NewLine Write-Host "" } function Draw-Sub-Header { param([string]$Title) Reset-Console Write-Host "" Write-TrueColor " :: $Title :: " 0 255 255 -NewLine Write-Host "" Draw-Separator } # ==================== 3. 基础工具 ==================== function Reset-Console { try { [Console]::ResetColor(); Clear-Host } catch { Clear-Host } } function Write-Line { param ([string]$Text, [ConsoleColor]$Color = "White") Write-Host " $Text" -ForegroundColor $Color } function Wait-Key { param([string]$Msg = "按任意键返回 (Esc 退出)...") Write-Host "`n $Msg" -ForegroundColor DarkGray while ($true) { if ([Console]::KeyAvailable) { if ([Console]::ReadKey($true).Key -eq "Escape") { return "Escape" } return "Any" } Start-Sleep -Milliseconds 50 } } function Read-Choice { param([string[]]$ValidKeys) while ($true) { if ([Console]::KeyAvailable) { $k = [Console]::ReadKey($true) if ($k.Key -eq "Escape") { return "Escape" } foreach ($vk in $ValidKeys) { if ($k.KeyChar.ToString().ToLower() -eq $vk.ToLower()) { return $vk } } } Start-Sleep -Milliseconds 50 } } function Check-Admin { if (-not (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "[!] 需要管理员权限" -ForegroundColor Red; exit } } # ==================== 4. 核心逻辑 ==================== function Ensure-WinSW { if (-not (Test-Path $SingBoxPath)) { Write-Host "❌ 缺 $ExeName.exe" -ForegroundColor Red; exit } if (-not (Test-Path $ServiceExe)) { try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-WebRequest -Uri $WinSWUrl -OutFile $ServiceExe -UseBasicParsing } catch { Write-Line "下载 WinSW 失败" "Red"; exit } } if (-not (Test-Path $ServiceXml)) { Set-Content $ServiceXml "$ServiceName$ServiceTitle%BASE%\$ExeName.exerun -c config.json30725" -Encoding UTF8 } } function Get-ServiceState { $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($null -eq $svc) { return "NotInstalled" } return $svc.Status.ToString() } function Start-Service-Wrapper { Ensure-WinSW; Archive-Old-Logs $state = Get-ServiceState if ($state -eq "Running") { Write-Line "服务已在运行" "Yellow"; return } if ($state -eq "NotInstalled") { Write-Line "首次运行,正在向系统注册服务..." "Magenta" Start-Process -FilePath $ServiceExe -ArgumentList "install" -Wait -NoNewWindow Start-Sleep -Seconds 1 } Write-Line "🚀 启动中..." "Cyan" Start-Process -FilePath $ServiceExe -ArgumentList "start" -Wait -NoNewWindow $retry = 0 while ($retry -lt 10) { if ((Get-ServiceState) -eq "Running") { Write-Line "✅ 服务启动成功" "Green" return } Start-Sleep -Milliseconds 500 $retry++ } Write-Line "⚠ 启动超时,请按 [6] 查看完整日志排查错误。" "Red" } function Stop-Service-Wrapper { if ((Get-ServiceState) -eq "Running") { Write-Line "🛑 停止中..." "Red" Start-Process -FilePath $ServiceExe -ArgumentList "stop" -Wait -NoNewWindow } else { Write-Line "未运行" "DarkGray" } } function Restart-Service-Wrapper { Stop-Service-Wrapper; Start-Sleep 1; Start-Service-Wrapper } function Update-WinSW { Draw-Sub-Header "更新内核" Write-Line "即将停止服务并更新 Service Wrapper" "Yellow" Write-Host "`n 确认更新? (Y/N)" -ForegroundColor DarkGray if ((Read-Choice "y", "n") -eq "y") { Stop-Service-Wrapper try { Invoke-WebRequest -Uri $WinSWUrl -OutFile $ServiceExe -UseBasicParsing Write-Line "✅ 更新成功" "Green" } catch { Write-Line "❌ 失败: $_" "Red" } Wait-Key | Out-Null } } function Set-AutoStart { Draw-Sub-Header "开机自启配置" # 1. 增加当前状态的显示 $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Write-Host " 当前状态 : " -NoNewline -ForegroundColor DarkGray if ($task) { Write-Host "延迟启动 (Delayed Task)" -ForegroundColor Magenta } elseif ($svc -and $svc.StartType -eq "Automatic") { Write-Host "标准自启 (Windows Service)" -ForegroundColor Cyan } else { Write-Host "已禁用 (Manual)" -ForegroundColor Gray } Write-Host "`n" Write-Host " [1] " -NoNewline -ForegroundColor White; Write-Host "标准自启 (Windows Service)" -ForegroundColor Cyan Write-Host " [2] " -NoNewline -ForegroundColor White; Write-Host "延迟启动 (可自定义延迟时间)" -ForegroundColor Magenta Write-Host " [3] " -NoNewline -ForegroundColor White; Write-Host "禁用自启 (仅手动触发)" -ForegroundColor Gray Write-Host "`n 请选择 (1-3) 或按 Esc 返回: " -NoNewline -ForegroundColor Yellow $c = Read-Choice "1","2","3" if ($c -eq "Escape") { return } Write-Host $c -ForegroundColor White $delay = 30 if ($c -eq "2") { Write-Host "`n 请输入开机后等待的秒数 (默认 30, 直接回车使用默认): " -NoNewline -ForegroundColor Cyan $inputDelay = Read-Host if ($inputDelay -match '^\d+$') { $delay = $inputDelay } } Write-Host "`n ⚙️ 正在应用配置,请稍候..." -ForegroundColor DarkGray if ($task) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } switch ($c) { "1" { Set-Service -Name $ServiceName -StartupType Automatic Write-Host " ✅ 配置成功: 已设为[标准自启]" -ForegroundColor Green } "2" { Set-Service -Name $ServiceName -StartupType Manual $act = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-WindowStyle Hidden -Command `"Start-Sleep -s $delay; Start-Service '$ServiceName'`"" Register-ScheduledTask -TaskName $TaskName -Action $act -Trigger (New-ScheduledTaskTrigger -AtLogOn) -Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest) -Force | Out-Null Write-Host " ✅ 配置成功: 已设为 [延迟启动] (开机后等待 $delay 秒)" -ForegroundColor Green } "3" { Set-Service -Name $ServiceName -StartupType Manual Write-Host " ✅ 配置成功: 已设为 [禁用自启]" -ForegroundColor Yellow } } Wait-Key | Out-Null } function Show-Restart-Menu { Draw-Sub-Header "重启选项" Write-Host " [1] " -NoNewline -ForegroundColor White; Write-Host "强制重启 (Direct Restart)" -ForegroundColor Red Write-Host " [2] " -NoNewline -ForegroundColor White; Write-Host "安全重载 (Safe Reload)" -ForegroundColor Green Write-Host "`n 请选择 (1-2) 或按 Esc 返回: " -NoNewline -ForegroundColor Yellow $c = Read-Choice "1","2" if ($c -eq "Escape") { return } Write-Host $c -ForegroundColor White if ($c -eq "1") { Write-Host "`n 🔄 正在执行强制重启..." -ForegroundColor DarkGray Restart-Service-Wrapper Wait-Key | Out-Null } elseif ($c -eq "2") { Write-Host "`n ⚙️ 正在执行安全校验..." -ForegroundColor DarkGray if (Check-Config-Silent) { Write-Host " ✔ 校验通过,正在备份并重启服务..." -ForegroundColor Green Backup-Config-Wrapper Restart-Service-Wrapper } else { Write-Host " ❌ 配置校验失败,已取消重启以保护服务运行!" -ForegroundColor Red } Wait-Key | Out-Null } } function Enter-TrayMode { Draw-Sub-Header "托盘模式" Write-Host " ⬇️ 正在将控制台隐藏至右下角系统托盘..." -ForegroundColor Cyan Write-Host " 💡 提示: 在右下角托盘图标处双击即可恢复控制台。" -ForegroundColor DarkGray Start-Sleep -Seconds 1 Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $signature = @" [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); "@ $Win32 = Add-Type -MemberDefinition $signature -Name "Win32Wnd" -Namespace "Win32" -PassThru $hWnd = $Win32::GetForegroundWindow() $Win32::ShowWindow($hWnd, 0) | Out-Null $tray = New-Object System.Windows.Forms.NotifyIcon $tray.Icon = [System.Drawing.SystemIcons]::Shield $tray.Text = "Sing-box Manager`n服务正在后台运行" $tray.Visible = $true $menu = New-Object System.Windows.Forms.ContextMenu $itemShow = New-Object System.Windows.Forms.MenuItem("显示面板 (Show)") $itemShow.add_Click({ $Win32::ShowWindow($hWnd, 9) | Out-Null $tray.Visible = $false; $tray.Dispose() [System.Windows.Forms.Application]::ExitThread() }) $menu.MenuItems.Add($itemShow) $menu.MenuItems.Add("-") $itemExit = New-Object System.Windows.Forms.MenuItem("完全退出 (Exit)") $itemExit.add_Click({ $tray.Visible = $false; $tray.Dispose() [System.Windows.Forms.Application]::ExitThread() [Environment]::Exit(0) }) $menu.MenuItems.Add($itemExit) $tray.ContextMenu = $menu $tray.add_DoubleClick({ $Win32::ShowWindow($hWnd, 9) | Out-Null $tray.Visible = $false; $tray.Dispose(); [System.Windows.Forms.Application]::ExitThread() }) [System.Windows.Forms.Application]::Run() } # ==================== 5. 日志/配置/网络 ==================== function Backup-Config-Wrapper { if (Test-Path $ConfigPath) { if (-not (Test-Path $ConfigBackupDir)) { New-Item -Type Directory -Path $ConfigBackupDir | Out-Null } Copy-Item $ConfigPath "$ConfigBackupDir\config_$(Get-Date -f 'yyyyMMdd_HHmmss').json" -Force } } function Archive-Old-Logs { param($Threshold = 0) # 默认0为无条件归档,设为3则达到3个才归档 if (-not (Test-Path $LogArchiveDir)) { New-Item -Type Directory -Path $LogArchiveDir -ErrorAction SilentlyContinue | Out-Null } $logs = Get-ChildItem $ScriptDir -Filter "$ServiceBase.*.err.log" -ErrorAction SilentlyContinue | ? { $_.Name -match "$ServiceBase\.\d+\.err\.log$" } if ($Threshold -gt 0 -and $logs.Count -lt $Threshold) { return $false } if ($logs.Count -eq 0) { return $false } $oldProg = $ProgressPreference $ProgressPreference = 'SilentlyContinue' foreach ($l in $logs) { try { $f = "$LogArchiveDir\$(Get-Date -f 'yyyy-MM')" if (-not (Test-Path $f)) { New-Item -Type Directory -Path $f -ErrorAction SilentlyContinue | Out-Null } Compress-Archive -Path $l.FullName -DestinationPath "$f\$($l.Name)_$(Get-Date -f 'HHmmss').zip" -Force -ErrorAction Stop Remove-Item $l.FullName -Force } catch {} } $ProgressPreference = $oldProg return $true } function Search-Log-Internal { param([string]$Keyword) Draw-Sub-Header "日志搜索: $Keyword" if (-not (Test-Path $LogFile)) { return } # 同样使用 ANSI 色彩加速 $E = [char]27 $c_cyan = "$E[36m"; $c_red = "$E[31m"; $c_yellow = "$E[33m" $c_white = "$E[37m"; $c_gray = "$E[90m"; $c_rst = "$E[0m" try { $matches = Select-String -Path $LogFile -Pattern $Keyword -Context 1,1 | Select-Object -Last 100 if ($matches) { $lineMap = @{} foreach ($m in $matches) { $cur = $m.LineNumber if ($m.Context.PreContext) { if (-not $lineMap[$cur-1]) { $lineMap[$cur-1] = @{T=$m.Context.PreContext[0];M=$false} } } $lineMap[$cur] = @{T=$m.Line;M=$true} if ($m.Context.PostContext) { if (-not $lineMap[$cur+1]) { $lineMap[$cur+1] = @{T=$m.Context.PostContext[0];M=$false} } } } $last = -1 foreach ($n in ($lineMap.Keys | Sort)) { $txt = $lineMap[$n].T.Trim() -replace "\+0800\s*", "" $pfx = "[{0}]" -f $n if ($last -ne -1 -and $n -ne ($last+1)) { Write-Host " -------" -ForegroundColor DarkGray } if ($lineMap[$n].M) { if ($txt -match 'error|fatal|panic') { Write-Host "${c_cyan}${pfx} ${c_red}>> ${txt}${c_rst}" } elseif ($txt -match 'warn') { Write-Host "${c_cyan}${pfx} ${c_yellow}>> ${txt}${c_rst}" } else { Write-Host "${c_cyan}${pfx} ${c_white}>> ${txt}${c_rst}" } } else { Write-Host "${c_gray}${pfx} ${txt}${c_rst}" } $last = $n } } else { Write-Line "未找到结果" "Yellow" } } catch { Write-Line "搜索错误: $_" "Red" } Wait-Key | Out-Null } function View-Log { $filterWarn = $false function Header { Reset-Console Write-Host " --- 实时日志流 --- (Live Log) " -NoNewline -BackgroundColor Yellow -ForegroundColor Black Write-Host " $LogFile " -BackgroundColor Black -ForegroundColor DarkGray $st = if($filterWarn){"开启"}else{"关闭"} Write-Host " [F]过滤警告:$st [C]清空[R]重载 [S]搜索 [Esc]退出 " -ForegroundColor White -BackgroundColor DarkGray Draw-Separator } Header if (-not (Test-Path $LogFile)) { Wait-Key | Out-Null; return } $curLn = 0; try { $curLn = (Get-Content $LogFile | Measure -Line).Lines } catch {} $reader = $null; $stream = $null; $lastSz = (Get-Item $LogFile).Length #[性能优化] 引入 ANSI 转义序列,避免 Write-Host 频繁调用导致控制台崩溃 $E = [char]27 $c_cyan = "$E[36m"; $c_red = "$E[31m"; $c_yellow = "$E[33m" $c_white = "$E[37m"; $c_gray = "$E[90m"; $c_rst = "$E[0m" try { $stream = [System.IO.File]::Open($LogFile, 'Open', 'Read', 'ReadWrite') $reader = New-Object System.IO.StreamReader($stream) $reader.BaseStream.Seek(0,[System.IO.SeekOrigin]::End) | Out-Null while ($true) { # [容灾机制] 内部包裹 Try-Catch,抵御瞬间海量日志造成的缓冲溢出 try { $line = $reader.ReadLine() if ($line) { $line = $line -replace "\+0800\s*", ""; $curLn++ $pfx = "[{0}]" -f $curLn $imp = ($line -match "WARN|ERROR|FATAL|PANIC") if (-not ($filterWarn -and -not $imp)) { # 将前缀和日志内容合并为一条字符串进行高速渲染 if ($line -match "ERROR|FATAL|panic") { Write-Host "${c_cyan}${pfx} ${c_red}${line}${c_rst}" } elseif ($line -match "WARN") { Write-Host "${c_cyan}${pfx} ${c_yellow}${line}${c_rst}" } elseif ($line -match "INFO") { Write-Host "${c_cyan}${pfx} ${c_white}${line}${c_rst}" } else { Write-Host "${c_cyan}${pfx} ${c_gray}${line}${c_rst}" } } } else { Start-Sleep -Milliseconds 100 try { $nowSz = (Get-Item $LogFile).Length if ($nowSz -lt $lastSz) { Write-Line ">>> 日志轮转重置 <<<" "Magenta" $reader.Close(); $stream.Close(); Start-Sleep -Milliseconds 200 $stream = [System.IO.File]::Open($LogFile, 'Open', 'Read', 'ReadWrite') $reader = New-Object System.IO.StreamReader($stream) $curLn = 0; $lastSz = $nowSz } else { $lastSz = $nowSz } } catch {} } if ([Console]::KeyAvailable) { $k = [Console]::ReadKey($true).Key if ($k -eq "Escape") { break } if ($k -eq "F") { $filterWarn = -not $filterWarn; Header } if ($k -eq "S") { $reader.Close(); $stream.Close() $kw = Read-Host "`n 搜索关键词"; if($kw){Search-Log-Internal $kw} Header; $stream=[System.IO.File]::Open($LogFile,'Open','Read','ReadWrite');$reader=New-Object System.IO.StreamReader($stream);$reader.BaseStream.Seek(0,[System.IO.SeekOrigin]::End)|Out-Null;$curLn=(Get-Content $LogFile|Measure -Line).Lines } if ($k -eq "C") { $reader.Close(); $stream.Close() try{Clear-Content $LogFile -ErrorAction Stop;$curLn=0}catch{}; Header $stream=[System.IO.File]::Open($LogFile,'Open','Read','ReadWrite');$reader=New-Object System.IO.StreamReader($stream);$reader.BaseStream.Seek(0,[System.IO.SeekOrigin]::End)|Out-Null;$lastSz=(Get-Item $LogFile).Length } if ($k -eq "R") { $reader.Close(); $stream.Close(); Header $curLn=(Get-Content $LogFile|Measure -Line).Lines $stream=[System.IO.File]::Open($LogFile,'Open','Read','ReadWrite');$reader=New-Object System.IO.StreamReader($stream);$reader.BaseStream.Seek(0,[System.IO.SeekOrigin]::End)|Out-Null;$lastSz=(Get-Item $LogFile).Length } } } catch { # 遇到控制台输出瓶颈时,短暂休眠,防止崩溃闪退 Start-Sleep -Milliseconds 50 } } } finally { if($reader){$reader.Close()}; if($stream){$stream.Close()} } } function Select-Config { Draw-Sub-Header "切换配置" $cfgs = Get-ChildItem $ScriptDir -Filter "*.json" | ? { $_.Name -notin "service.json","stats.json" -and $_.Name -notmatch "singbox-service|config_20" } if ($cfgs.Count -eq 0) { Write-Line "无其他配置文件" "Red"; Wait-Key|Out-Null; return } Write-Line "当前: config.json" "DarkGray"; Write-Host "" for ($i=0; $i -lt $cfgs.Count; $i++) { $n = $cfgs[$i].Name Write-Host " " -NoNewline Write-Host " [$($i+1)] " -ForegroundColor Black -BackgroundColor Cyan -NoNewline Write-Host " $n " -ForegroundColor Cyan } # [修复] 增加返回提示,使用黄色高亮 Write-Host "`n 选择序号 (0 或 Esc 返回): " -NoNewline -ForegroundColor Yellow $in = Read-Host; # 支持输入 0 或 Esc 逻辑 if (-not $in -match '^\d+$' -or $in -eq "0") { return } $sel = $cfgs[[int]$in - 1] if ($sel.Name -ne "config.json") { Backup-Config-Wrapper Copy-Item $sel.FullName $ConfigPath -Force Set-Content $ConfigNameFile $sel.Name -Force Write-Line "✅ 已切换为 $($sel.Name)" "Green" Write-Host " 立即重启? (Y/N)" -ForegroundColor DarkGray if ((Read-Choice "y","n") -eq "y") { Restart-Service-Wrapper } } } function Show-Monitor { Reset-Console; try{[Console]::CursorVisible=$false}catch{} $apiPort=$null; $apiSec=""; $cfgName="config.json" if(Test-Path $ConfigPath){try{$j=Get-Content $ConfigPath -Raw|ConvertFrom-Json;$apiPort=($j.experimental.clash_api.external_controller -split ":")[-1];$apiSec=$j.experimental.clash_api.secret}catch{}} if(Test-Path $ConfigNameFile){$cfgName=(Get-Content $ConfigNameFile -Raw).Trim()} $lUp=0; $lDown=0; $firstRun=$true; $lastRot=Get-Date; $rotMsg=""; $cachedPid=0 while($true) { [Console]::SetCursorPosition(0,0) Write-Host " 实时监控面板 (Monitor) " -NoNewline -B DarkCyan -F Black Write-Host " [Esc]返回 [L]日志 [R]重载 " -B Black -F White Draw-Separator # 日志轮转逻辑 (复刻截图逻辑) if(((Get-Date)-$lastRot).TotalSeconds -gt 2){ $r = Get-ChildItem $ScriptDir -Filter "$ServiceBase.*.err.log" | ? {$_.Name -match "$ServiceBase\.\d+\.err\.log$"} if($r){ if($r.Count -ge 3){ Archive-Old-Logs; $rotMsg="📦 已将旧日志自动打包至 archives" } else { $rotMsg="⚠ 日志已轮转, 待归档进度: $($r.Count)/3" } } else { $rotMsg="" } $lastRot=Get-Date } if($rotMsg){Write-Host " $rotMsg$(' '*30)" -F Yellow}else{Write-Host " $(' '*60)"} Write-Host "--------------------------------------------------------" -F DarkGray $proc=$null try{ if($cachedPid -gt 0){ $proc=Get-Process -Id $cachedPid -EA Stop } else{ if(Test-Path $PidFile){$pidVal=[int](Get-Content $PidFile -EA 0);$proc=Get-Process -Id $pidVal -EA Stop;$cachedPid=$pidVal} if(!$proc){$proc=Get-Process -Name $ExeName -EA Stop|select -First 1;$cachedPid=$proc.Id} } }catch{$cachedPid=0} if($proc){ $uptime="00:00:00"; try{$u=(Get-Date)-$proc.StartTime;$uptime="{0:D2}:{1:D2}:{2:D2}" -f $u.Hours,$u.Minutes,$u.Seconds;if($u.Days -gt 0){$uptime="$($u.Days)天 $uptime"}}catch{} $mem="{0:N2}" -f ($proc.WorkingSet64/1MB) $spU="0 KB/s"; $spD="0 KB/s"; $totU="0 MB"; $totD="0 MB"; $tcp="N/A" if($apiPort){ try{ $s=Invoke-RestMethod -Uri "http://127.0.0.1:$apiPort/connections" -Headers @{Authorization="Bearer $apiSec"} -TimeoutSec 1 -EA Stop $cu=$s.uploadTotal; $cd=$s.downloadTotal if(!$firstRun -and $cu -ge $lUp){ $du=$cu-$lUp; $dd=$cd-$lDown $spU=if($du -gt 1MB){"{0:N2} MB/s" -f ($du/1MB)}else{"{0:N0} KB/s" -f ($du/1KB)} $spD=if($dd -gt 1MB){"{0:N2} MB/s" -f ($dd/1MB)}else{"{0:N0} KB/s" -f ($dd/1KB)} } $lUp=$cu; $lDown=$cd; $firstRun=$false $totU=if($cu -gt 1GB){"{0:N2} GB" -f ($cu/1GB)}else{"{0:N2} MB" -f ($cu/1MB)} $totD=if($cd -gt 1GB){"{0:N2} GB" -f ($cd/1GB)}else{"{0:N2} MB" -f ($cd/1MB)} if($s.connections){$tcp=$s.connections.Count} }catch{$spU="-";$tcp="Err"} } $pad=" "*20 Write-Host " ● 服务状态 : " -NoNewline -F Gray; Write-Host "Running$pad" -F Green Write-Host " 🔎 进程PID : $($proc.Id)$pad" -F Magenta Write-Host " ⏱ 运行时长 : $uptime$pad" -F Yellow Write-Host " 💾 内存占用 : $mem MB$pad" -F Cyan Write-Host " 🔗 活跃连接 : $tcp (API)$pad" -F Blue Write-Host "" Write-Host " [ 🚀 流量统计 (API:$apiPort) ]$pad" -F Green Write-Host (" ⬆ 上传 : {0,-12} (总: {1})$pad" -f $spU,$totU) -F Gray Write-Host (" ⬇ 下载 : {0,-12} (总: {1})$pad" -f $spD,$totD) -F White } else { Write-Host " ● 服务状态 : " -NoNewline -F Gray; Write-Host "Stopped$(' '*20)" -F Red Write-Host "`n`n`n`n`n" } Write-Host "`n"; Draw-Separator for($i=0;$i -lt 10;$i++){ if([Console]::KeyAvailable){ $k=[Console]::ReadKey($true).Key; if($k -eq "Escape"){return} if($k -eq "L"){try{[Console]::CursorVisible=$true}catch{};View-Log;Reset-Console;try{[Console]::CursorVisible=$false}catch{}} if($k -eq "R"){Reset-Console} } Start-Sleep -Milliseconds 100 } } try{[Console]::CursorVisible=$true}catch{} } function Test-AdvancedNetwork { Draw-Sub-Header "网络诊断" # [修复] 增加提示文案 Write-Host " 提示: 检测过程中按 [Esc] 可强制中止" -ForegroundColor DarkGray Write-Host "" function ChkEsc { if([Console]::KeyAvailable -and [Console]::ReadKey($true).Key -eq "Escape"){Write-Line "中止" "Red"; return $true};return $false } $sp=1080; if(Test-Path $ConfigPath){try{$j=Get-Content $ConfigPath -Raw|ConvertFrom-Json;$sp=if($j.inbounds[0].listen_port){$j.inbounds[0].listen_port}else{$j.inbounds[0].port}}catch{}} Write-Line "[ 直连 DNS ]" "Cyan" foreach($d in "baidu.com","microsoft.com"){ if(ChkEsc){return}; Write-Host " $d : " -NoNewline try{ $i=[System.Net.Dns]::GetHostAddresses($d)|select -First 1; if($i){Write-Host "OK" -F Green}else{Write-Host "Fail" -F Red} }catch{Write-Host "Err" -F Red} } Write-Line "`n[ 本地代理 : $sp ]" "Cyan" if(Get-NetTCPConnection -LocalPort $sp -ErrorAction SilentlyContinue){Write-Line "端口监听: OK" "Green"}else{Write-Line "端口监听: Fail" "Red"} Write-Line "[ HTTP 延迟 ]" "Cyan" foreach($t in @(@{N="Google";U="http://google.com/gen_204"},@{N="GitHub";U="https://github.com"})){ if(ChkEsc){return}; Write-Host " $($t.N) : " -NoNewline try{ $req=[System.Net.WebRequest]::Create($t.U); $req.Timeout=3000; $req.Method="HEAD" $req.Proxy=New-Object System.Net.WebProxy("127.0.0.1:$sp") $sw=[System.Diagnostics.Stopwatch]::StartNew(); $null=$req.GetResponse(); $sw.Stop() Write-Host "$($sw.ElapsedMilliseconds)ms" -F Green }catch{Write-Host "Timeout/Err" -F Red} } Wait-Key | Out-Null } function Toggle-SystemProxy { param([string]$Mode = "Toggle") $Reg = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" $cur = (Get-ItemProperty $Reg ProxyEnable -EA 0).ProxyEnable $set = -1 if ($Mode -eq "Toggle") { $set = if ($cur -eq 1) { 0 }else { 1 } } elseif ($Mode -eq "On" -and $cur -ne 1) { $set = 1 } elseif ($Mode -eq "Off" -and $cur -ne 0) { $set = 0 } if ($set -eq 1) { Draw-Sub-Header "系统代理: 开启" $p = 7890; try { $j = Get-Content $ConfigPath -Raw | ConvertFrom-Json; $p = $j.inbounds | ? { $_.type -match "mixed|http" } | select -exp listen_port -First 1 }catch {} Set-ItemProperty $Reg ProxyEnable 1; Set-ItemProperty $Reg ProxyServer "127.0.0.1:$p" Write-Line "已开启 (127.0.0.1:$p)" "Green" } elseif ($set -eq 0) { if ($Mode -eq "Toggle") { Draw-Sub-Header "系统代理: 关闭"; Write-Line "已关闭" "Yellow" } Set-ItemProperty $Reg ProxyEnable 0 } try { $sig = '[DllImport("wininet.dll",SetLastError=true)]public static extern bool InternetSetOption(IntPtr h,int o,IntPtr b,int l);' $t = Add-Type -MemberDefinition $sig -Name "WinInet" -Namespace "Win" -PassThru $t::InternetSetOption([IntPtr]::Zero, 39, [IntPtr]::Zero, 0) $t::InternetSetOption([IntPtr]::Zero, 37, [IntPtr]::Zero, 0) } catch {} if ($Mode -eq "Toggle") { Start-Sleep 1 } } function Check-Config-Silent { try { return (Start-Process $SingBoxPath "check -c `"$ConfigPath`"" -NoNewWindow -Wait -PassThru).ExitCode -eq 0 }catch { return $false } } function Check-Config { Draw-Sub-Header "配置校验"; if (Check-Config-Silent) { Write-Line "✔ 校验通过" "Green" }else { Write-Line "❌ 校验失败" "Red" }; Wait-Key | Out-Null } # ==================== 6. 主菜单 ==================== function Show-Menu { Reset-Console; Draw-Gradient-Art; Write-TrueColor " ════════════════════════════════════════════════════════════════" 80 0 80 -NewLine $st=Get-ServiceState; $pr=(Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" ProxyEnable -EA 0).ProxyEnable Write-Host " "; if($st -eq "Running"){Write-Host " ● CORE: ONLINE " -NoNewline -B Green -F Black}else{Write-Host " ● CORE: OFFLINE " -NoNewline -B Red -F White} Write-Host " "; if($pr -eq 1){Write-Host " ⇄ PROXY: ON " -NoNewline -B Cyan -F Black}else{Write-Host " ⇄ PROXY: OFF " -NoNewline -B DarkGray -F White} Write-Host " " -NoNewline; Write-TrueColor ":: SYSTEM READY" 100 100 100 -NewLine; Write-Host "" function Btn($id, $cn, $color, $nl=$false) { Write-Host " $id " -NoNewline -BackgroundColor $color -ForegroundColor Black Write-Host " $cn" -NoNewline -ForegroundColor $color if (-not $nl) { $cjkCount = ([regex]::Matches($cn, '[\p{IsCJKUnifiedIdeographs}]')).Count $visLen = 3 + 1 + $cn.Length + $cjkCount $pad = 28 - $visLen if ($pad -lt 2) { $pad = 2 } Write-Host (" " * $pad) -NoNewline } else { Write-Host "" } } Write-Host " [ 核心控制 ]" -ForegroundColor DarkGray Btn "1" "启动服务" "Green" Btn "2" "停止服务" "Red" $true Btn "3" "重启服务" "Yellow" Btn "4" "实时监控" "Cyan" $true Write-Host "" Write-Host " [ 功能管理 ]" -ForegroundColor DarkGray Btn "5" "切换配置" "Magenta" Btn "6" "查看日志" "Darkblue" $true Btn "7" "网络诊断" "Blue" Btn "8" "配置校验" "Gray" $true Write-Host "" Write-Host " [ 系统维护 ]" -ForegroundColor DarkGray Btn "A" "系统代理开关" "DarkYellow" Btn "B" "更新 WinSW 内核" "DarkYellow" $true Btn "C" "开机自启设置" "DarkYellow" $true Write-Host "`n ════════════════════════════════════════════════════════════════" -ForegroundColor DarkGray Write-Host " 0. 停止并退出 Q. 仅退出 T. 最小化至托盘" -ForegroundColor Gray } # ==================== 7. 入口 ==================== Check-Admin; Ensure-WinSW if ($Start) { Start-Service-Wrapper; exit }; if ($Stop) { Stop-Service-Wrapper; exit }; if ($Restart) { Restart-Service-Wrapper; exit }; if ($Monitor) { Show-Monitor; exit } while($true) { Show-Menu; Write-Host "`n 指令 > " -NoNewline -F DarkGray $c = Read-Choice "1","2","3","4","5","6","7","8","a","b","c","0","q","t" switch($c) { "1" { Start-Service-Wrapper; Wait-Key|Out-Null } "2" { Stop-Service-Wrapper; Wait-Key|Out-Null } "3" { Show-Restart-Menu } "4" { Show-Monitor } "5" { Select-Config } "6" { View-Log } "7" { Test-AdvancedNetwork } "8" { Check-Config } "a" { Toggle-SystemProxy } "b" { Update-WinSW } "c" { Set-AutoStart } "t" { Enter-TrayMode } "0" { Toggle-SystemProxy "Off"; Stop-Service-Wrapper; if(Test-Path $ConfigNameFile){Del $ConfigNameFile -Force}; exit } "q" { if(Test-Path $ConfigNameFile){Del $ConfigNameFile -Force}; exit } "Escape" { if(Test-Path $ConfigNameFile){Del $ConfigNameFile -Force}; exit } } }