Files
OnebotCatalog/tools/serve-web.ps1

112 lines
4.6 KiB
PowerShell

# 本地预览服务器 (零依赖 TcpListener + Runspace 池并发): 服务在线站点静态目录
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web.ps1 [-Root release\web_2026.08] [-Port 8080]
# 并发: 每个请求一个 Runspace (池上限 16), 大文件 (wasm/图片) 不再阻塞小文件 (step 下载/页面)
param(
[string]$Root = (Join-Path $PSScriptRoot '..\release\web_2026.08'),
[int]$Port = 8080,
[string]$ViewerRoot = (Join-Path $PSScriptRoot '..\viewer') # /viewer/ 前缀 → 新迪查看器本地副本 (可选; 原项目 Z:\...\上海新迪3D\dev 只读)
)
if (-not (Test-Path $Root)) { Write-Output "目录不存在: $Root"; exit 1 }
$viewerRootFull = ''
if ($ViewerRoot -and (Test-Path $ViewerRoot)) {
$viewerRootFull = [System.IO.Path]::GetFullPath($ViewerRoot)
Write-Output ("查看器目录: " + $viewerRootFull)
}
$mime = @{
'.html' = 'text/html; charset=utf-8'; '.js' = 'application/javascript; charset=utf-8'
'.css' = 'text/css; charset=utf-8'; '.json' = 'application/json; charset=utf-8'
'.png' = 'image/png'; '.jpg' = 'image/jpeg'; '.pdf' = 'application/pdf'
'.step' = 'application/octet-stream'; '.ico' = 'image/x-icon'; '.txt' = 'text/plain; charset=utf-8'
'.wasm' = 'application/wasm'; '.stp' = 'application/octet-stream'
}
$rootFull = [System.IO.Path]::GetFullPath($Root)
# 单请求处理逻辑 (自包含, 在 Runspace 中执行)
$handlerText = @'
param($client, $rootFull, $viewerRootFull, $mime)
try {
$stream = $client.GetStream()
$stream.ReadTimeout = 2000 # 预连接/空连接快速失败, 不阻塞后续请求
$stream.WriteTimeout = 30000
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 4096, $true)
$reqLine = $reader.ReadLine()
if ($null -eq $reqLine) { $client.Close(); return } # 客户端连上不发数据, 直接断开
# 读掉请求头 (EOF 即 null 时必须退出, 否则死循环)
$line = $reader.ReadLine()
$guard = 0
while ($line -ne $null -and $line -ne '') {
$line = $reader.ReadLine()
$guard++
if ($guard -gt 100) { break }
}
$path = '/'
if ($reqLine) {
$parts = $reqLine -split ' '
if ($parts.Count -ge 2) { $path = $parts[1] }
}
# 去掉查询串 (?v= / ?file= 等), 否则会进文件路径导致非法字符异常
$qi = $path.IndexOf('?')
if ($qi -ge 0) { $path = $path.Substring(0, $qi) }
if ($path -eq '/') { $path = '/index.html' }
$full = [System.IO.Path]::GetFullPath((Join-Path $rootFull ($path -replace '/', '\').TrimStart('\')))
$bytes = $null; $status = '200 OK'; $ctype = 'application/octet-stream'
# /viewer/ 前缀 → 新迪 3D 查看器根目录 (各自防目录穿越)
$rootNow = $rootFull
$rel = ($path -replace '/', '\').TrimStart('\')
if ($viewerRootFull -and $rel.StartsWith('viewer\', [System.StringComparison]::OrdinalIgnoreCase)) {
$rootNow = $viewerRootFull
$rel = $rel.Substring('viewer\'.Length)
$full = [System.IO.Path]::GetFullPath((Join-Path $rootNow $rel))
}
if ($full.StartsWith($rootNow, [System.StringComparison]::OrdinalIgnoreCase) -and (Test-Path $full -PathType Leaf)) {
$bytes = [System.IO.File]::ReadAllBytes($full)
$ext = [System.IO.Path]::GetExtension($full).ToLowerInvariant()
if ($mime.ContainsKey($ext)) { $ctype = $mime[$ext] }
} else {
$status = '404 Not Found'
$bytes = [System.Text.Encoding]::UTF8.GetBytes('404 Not Found')
$ctype = 'text/plain; charset=utf-8'
}
$head = "HTTP/1.1 $status`r`nContent-Type: $ctype`r`nCache-Control: no-cache`r`nContent-Length: $($bytes.Length)`r`nConnection: close`r`n`r`n"
$headBytes = [System.Text.Encoding]::ASCII.GetBytes($head)
$stream.Write($headBytes, 0, $headBytes.Length)
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
} catch { }
$client.Close()
'@
# Runspace 池 (并发上限 16)
$pool = [runspacefactory]::CreateRunspacePool(1, 16)
$pool.Open()
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
$listener.Start()
Write-Output ("服务已启动: http://localhost:" + $Port + " (停止: 关闭本窗口)")
while ($true) {
$client = $null
try {
$client = $listener.AcceptTcpClient()
} catch {
# 监听异常不退出进程 (端口占用等), 2 秒后重试
Start-Sleep -Seconds 2
continue
}
try {
$ps = [powershell]::Create()
$ps.RunspacePool = $pool
[void]$ps.AddScript($handlerText)
[void]$ps.AddArgument($client)
[void]$ps.AddArgument($rootFull)
[void]$ps.AddArgument($viewerRootFull)
[void]$ps.AddArgument($mime)
[void]$ps.BeginInvoke()
} catch {
$client.Close()
}
}