首次提交: OnebotCatalog 项目代码与文档(含 NX 按需生成服务二期、后台、一键启动)
This commit is contained in:
111
tools/serve-web.ps1
Normal file
111
tools/serve-web.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
# 本地预览服务器 (零依赖 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user