首次提交
Azure Static Web Apps CI/CD / Close Pull Request Job (push) Skipped
Build and Deploy / build (push) Failing after 9m48s
Azure Static Web Apps CI/CD / Build and Deploy Job (push) Failing after 9m50s

This commit is contained in:
2026-10-24 01:29:24 +08:00
parent e70d46436c
commit c7fdc11dbd
146 changed files with 9750 additions and 5578 deletions
+915
View File
@@ -0,0 +1,915 @@
param(
[string]$Arg1 = "",
[string]$Arg2 = ""
)
# Set console encoding to UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$code = @"
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.IO;
public class ScreenCapture
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll")]
public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, int nFlags);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern uint MapVirtualKey(uint uCode, uint uMapType);
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
public static System.Collections.Generic.List<IntPtr> GetProcessWindows(uint processId) {
System.Collections.Generic.List<IntPtr> windows = new System.Collections.Generic.List<IntPtr>();
EnumWindows(delegate(IntPtr hWnd, IntPtr lParam) {
uint id;
GetWindowThreadProcessId(hWnd, out id);
if (id == processId && IsWindowVisible(hWnd) && GetWindowTextLength(hWnd) > 0) {
windows.Add(hWnd);
}
return true;
}, IntPtr.Zero);
return windows;
}
public static string GetWindowTextStr(IntPtr hWnd) {
int length = GetWindowTextLength(hWnd);
if (length == 0) return "";
System.Text.StringBuilder builder = new System.Text.StringBuilder(length + 1);
GetWindowText(hWnd, builder, builder.Capacity);
return builder.ToString();
}
public const int SW_RESTORE = 9;
public const int SW_SHOWNOACTIVATE = 4;
public const byte VK_LEFT = 0x25;
public const byte VK_RIGHT = 0x27;
public const uint KEYEVENTF_KEYUP = 0x0002;
public const uint WM_KEYDOWN = 0x0100;
public const uint WM_KEYUP = 0x0101;
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOMOVE = 0x0002;
public const uint SWP_NOACTIVATE = 0x0010;
public static void SendKeyPress(byte key)
{
keybd_event(key, 0, 0, UIntPtr.Zero);
System.Threading.Thread.Sleep(50);
keybd_event(key, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
private static IntPtr MakeKeyLParam(byte key, bool isKeyUp)
{
uint scanCode = MapVirtualKey(key, 0);
uint lParam = 1 | (scanCode << 16);
if (isKeyUp)
{
lParam |= (1u << 30);
lParam |= (1u << 31);
}
return (IntPtr)unchecked((int)lParam);
}
public static void SendKeyToWindow(IntPtr hWnd, byte key)
{
PostMessage(hWnd, WM_KEYDOWN, (IntPtr)key, MakeKeyLParam(key, false));
System.Threading.Thread.Sleep(50);
PostMessage(hWnd, WM_KEYUP, (IntPtr)key, MakeKeyLParam(key, true));
}
public static void SetNotTopMost(IntPtr hWnd)
{
SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
public static string CaptureWindow(IntPtr hWnd)
{
// 如果窗口最小化了,则先将其恢复,但不激活(不抢占焦点)
if (IsIconic(hWnd))
{
ShowWindow(hWnd, SW_SHOWNOACTIVATE);
// 稍微等待一下让窗口恢复
System.Threading.Thread.Sleep(100);
}
RECT rc;
GetWindowRect(hWnd, out rc);
int width = rc.Right - rc.Left;
int height = rc.Bottom - rc.Top;
if(width <= 0 || height <= 0) return null;
Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
Graphics gfxBmp = Graphics.FromImage(bmp);
IntPtr hdcBitmap = gfxBmp.GetHdc();
PrintWindow(hWnd, hdcBitmap, 2);
gfxBmp.ReleaseHdc(hdcBitmap);
gfxBmp.Dispose();
using (MemoryStream ms = new MemoryStream())
{
// Compress heavily for faster network
EncoderParameters encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 50L);
ImageCodecInfo jpegCodec = GetEncoder(ImageFormat.Jpeg);
bmp.Save(ms, jpegCodec, encoderParameters);
byte[] byteImage = ms.ToArray();
bmp.Dispose();
return Convert.ToBase64String(byteImage);
}
}
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
}
"@
try {
Add-Type -TypeDefinition $code -ReferencedAssemblies System.Drawing, System.Windows.Forms -ErrorAction SilentlyContinue
} catch {
Write-Host "Warning: Could not compile ScreenCapture class" -ForegroundColor Yellow
}
# --- Configuration Logic ---
$port = 3001
$currentDir = (Get-Location).Path
$root = ""
# Parse arguments logic similar to the requested CMD
# Logic: If Arg1 is numeric, it's the port. Otherwise it might be the path (or ignored based on CMD logic).
# If Arg2 is present, it's the path.
if ($Arg1 -match "^\d+$") {
$port = [int]$Arg1
} elseif ($Arg1 -ne "") {
# If first arg is not a number, assume it is the root path (enhancement over original CMD)
$root = $Arg1
}
if ($Arg2 -ne "") {
$root = $Arg2
}
# Auto-detect root if not specified
if ($root -eq "") {
if (Test-Path (Join-Path $currentDir "dist")) {
$root = Join-Path $currentDir "dist"
} elseif (Test-Path (Join-Path $currentDir "build")) {
$root = Join-Path $currentDir "build"
} else {
# Fallback to current directory if neither exists (though validation will fail later)
$root = Join-Path $currentDir "dist"
}
} else {
# Resolve relative path
if (-not [System.IO.Path]::IsPathRooted($root)) {
$root = Join-Path $currentDir $root
}
}
# --- Data Directory & Class Config ---
$baseDir = Split-Path $root -Parent
if ([string]::IsNullOrEmpty($baseDir) -or -not (Test-Path $baseDir)) {
$baseDir = $currentDir
}
$dataDir = Join-Path $baseDir "data"
if (-not (Test-Path $dataDir)) {
New-Item -ItemType Directory -Path $dataDir -Force | Out-Null
}
$classConfigFile = Join-Path $dataDir "classworks-config-basic.json"
$className = "未设置"
if (Test-Path $classConfigFile) {
try {
$rawConfig = Get-Content -Path $classConfigFile -Raw -Encoding UTF8
$classObj = $rawConfig | ConvertFrom-Json
if ($classObj.classNumber) {
$className = $classObj.classNumber
}
} catch {
# Ignore
}
}
# --- UI Display ---
Write-Host "========================================" -ForegroundColor White
Write-Host " 宝一外班级作业看板" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor White
Write-Host ""
Write-Host "关于:" -ForegroundColor White
Write-Host "ClassHomeworkBoard 班级作业看板由高一七班(原12班)李昊睿制作。在宝一外提供技术支持。" -ForegroundColor Gray
Write-Host "---" -ForegroundColor DarkGray
Write-Host "主机名称 : $env:COMPUTERNAME" -ForegroundColor Gray
Write-Host "当前用户 : $env:USERNAME" -ForegroundColor Gray
Write-Host "班级名称 : $className" -ForegroundColor Cyan
Write-Host "运行目录 : $root" -ForegroundColor Yellow
Write-Host "监听端口 : $port" -ForegroundColor Green
Write-Host "---" -ForegroundColor DarkGray
Write-Host ""
# --- Validation ---
if (-not (Test-Path $root)) {
Write-Host "[错误] 目录不存在: $root" -ForegroundColor Red
Write-Host "请先运行: npm run build" -ForegroundColor Red
exit 1
}
# --- Port Detection ---
function Test-Port($p) {
$tcp = New-Object System.Net.Sockets.TcpClient
try {
$tcp.Connect("localhost", $p)
$tcp.Close()
return $true
} catch {
return $false
}
}
while (Test-Port $port) {
Write-Host "[信息] 端口 $port 被占用,尝试端口 $($port + 1)..." -ForegroundColor Yellow
$port++
}
# --- Browser Detection ---
function Find-Edge {
# 1. Common C: paths
$paths = @(
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
"D:\CHWB-CI\Microsoft\Edge\Application\msedge.exe",
"$env:LOCALAPPDATA\Microsoft\Edge\Application\msedge.exe"
)
foreach ($path in $paths) {
if (Test-Path $path) { return $path }
}
# 2. Registry
try {
$regPath = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe" -ErrorAction SilentlyContinue
if ($regPath -and (Test-Path $regPath.'(default)')) {
return $regPath.'(default)'
}
} catch {}
# 3. Search D: drive (User request)
Write-Host "正在D盘查找 Microsoft Edge (msedge.exe),这可能需要一点时间..." -ForegroundColor Gray
try {
# Search D: drive for msedge.exe
$edge = Get-ChildItem -Path "D:\" -Filter "msedge.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($edge) {
return $edge.FullName
}
} catch {}
return $null
}
# --- Server Start ---
$url = "http://localhost:$port/"
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add($url)
try {
$listener.Start()
Write-Host "[√] 启动成功 $url" -ForegroundColor Green
Write-Host "正在自动打开浏览器..." -ForegroundColor DarkGray
Write-Host ""
} catch {
Write-Host "[错误] 启动服务器失败: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
try {
$edgePath = Find-Edge
if ($edgePath) {
Write-Host "使用 Edge 浏览器打开: $edgePath" -ForegroundColor Gray
Start-Process $edgePath $url -ErrorAction Stop
} else {
Write-Host "未找到 Edge,使用默认浏览器打开..." -ForegroundColor Gray
Start-Process $url -ErrorAction Stop
}
} catch {
Write-Host "[警告] 无法自动打开浏览器,请手动访问上述地址。" -ForegroundColor Yellow
}
# --- MIME Types ---
$mimeTypes = @{
".html" = "text/html; charset=utf-8"
".htm" = "text/html; charset=utf-8"
".css" = "text/css; charset=utf-8"
".js" = "application/javascript; charset=utf-8"
".mjs" = "application/javascript; charset=utf-8"
".json" = "application/json; charset=utf-8"
".png" = "image/png"
".jpg" = "image/jpeg"
".jpeg" = "image/jpeg"
".gif" = "image/gif"
".svg" = "image/svg+xml"
".ico" = "image/x-icon"
".woff" = "font/woff"
".woff2" = "font/woff2"
".ttf" = "font/ttf"
".eot" = "application/vnd.ms-fontobject"
".otf" = "font/otf"
".wasm" = "application/wasm"
".map" = "application/json"
".txt" = "text/plain; charset=utf-8"
".xml" = "text/xml; charset=utf-8"
".pdf" = "application/pdf"
".mp3" = "audio/mpeg"
".mp4" = "video/mp4"
".webm" = "video/webm"
".webp" = "image/webp"
}
# --- Request Loop ---
while ($listener.IsListening) {
try {
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
$path = $request.Url.LocalPath
# URL Decode
$path = [System.Net.WebUtility]::UrlDecode($path)
if ($path.StartsWith("/api/ppt/check")) {
$response.AddHeader("Access-Control-Allow-Origin", "*")
$response.AddHeader("Access-Control-Allow-Methods", "GET, OPTIONS")
$response.AddHeader("Access-Control-Allow-Headers", "Content-Type")
$response.AddHeader("Cache-Control", "no-store")
if ($request.HttpMethod -eq "OPTIONS") {
$response.StatusCode = 204
$response.Close()
continue
}
try {
$ppts = Get-Process -Name "wpp", "POWERPNT", "wps" -ErrorAction SilentlyContinue
if ($ppts) {
$pptList = @()
$seenHwnds = @{}
foreach ($p in $ppts) {
$procName = $p.ProcessName
$hwnds = [ScreenCapture]::GetProcessWindows($p.Id)
foreach ($hwnd in $hwnds) {
$hwndStr = $hwnd.ToString()
if ($seenHwnds.ContainsKey($hwndStr)) {
continue
}
$title = [ScreenCapture]::GetWindowTextStr($hwnd)
# Skip system/irrelevant windows
if ($title -eq "DI" -or $title -eq "Default IME" -or $title -eq "MSCTFIME UI" -or $title -eq "OleMainThreadWndName") {
continue
}
if ($procName -eq "POWERPNT" -and $title -notmatch "PowerPoint") {
continue
}
if ($procName -eq "wps") {
if ($title -match "WPS\s*文字" -or $title -match "WPS\s*表格") {
continue
}
if ($title -notmatch "WPS\s*演示" -and $title -notmatch "Presentation" -and $title -notmatch "PPT" -and $title -notmatch "幻灯片" -and $title -notmatch "Slide Show") {
continue
}
}
$seenHwnds[$hwndStr] = $true
if ([ScreenCapture]::IsIconic($hwnd)) {
[ScreenCapture]::ShowWindow($hwnd, [ScreenCapture]::SW_SHOWNOACTIVATE)
Start-Sleep -Milliseconds 100
}
$b64 = [ScreenCapture]::CaptureWindow($hwnd)
$isSlideShow = ($title -match "幻灯片放映" -or $title -match "Slide Show")
$pptInfo = @{ title = $title; slideShow = $isSlideShow }
if (-not [string]::IsNullOrEmpty($b64)) {
$pptInfo.image = "data:image/jpeg;base64,$b64"
}
$pptList += $pptInfo
}
}
$respObj = @{ success = $true; hasPpt = ($pptList.Length -gt 0); ppts = $pptList }
} else {
$respObj = @{ success = $true; hasPpt = $false; ppts = @() }
}
$respJson = $respObj | ConvertTo-Json -Compress -Depth 5
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
} catch {
$respObj = @{ success = $false; error = $_.Exception.Message }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 500
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
}
continue
}
if ($path.StartsWith("/api/ppt/control")) {
$response.AddHeader("Access-Control-Allow-Origin", "*")
$response.AddHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
$response.AddHeader("Access-Control-Allow-Headers", "Content-Type")
$response.AddHeader("Cache-Control", "no-store")
if ($request.HttpMethod -eq "OPTIONS") {
$response.StatusCode = 204
$response.Close()
continue
}
try {
$action = $request.QueryString["action"]
$ppts = Get-Process -Name "wpp", "POWERPNT", "wps" -ErrorAction SilentlyContinue
if ($ppts) {
$targetProc = $null
$hwnd = [IntPtr]::Zero
$procName = ""
foreach ($p in $ppts) {
$procNameCandidate = $p.ProcessName
$hwnds = [ScreenCapture]::GetProcessWindows($p.Id)
foreach ($h in $hwnds) {
$t = [ScreenCapture]::GetWindowTextStr($h)
if ($t -eq "DI" -or $t -eq "Default IME" -or $t -eq "MSCTFIME UI" -or $t -eq "OleMainThreadWndName") {
continue
}
if ($procNameCandidate -eq "POWERPNT" -and $t -notmatch "PowerPoint") {
continue
}
if ($procNameCandidate -eq "wps") {
if ($t -match "WPS\s*文字" -or $t -match "WPS\s*表格") {
continue
}
if ($t -notmatch "WPS\s*演示" -and $t -notmatch "Presentation" -and $t -notmatch "PPT" -and $t -notmatch "幻灯片" -and $t -notmatch "Slide Show") {
continue
}
}
$targetProc = $p
$hwnd = $h
$procName = $procNameCandidate
break
}
if ($targetProc -and $hwnd -ne [IntPtr]::Zero) {
break
}
}
if (-not $targetProc -or $hwnd -eq [IntPtr]::Zero) {
$respObj = @{ success = $false; error = "No PPT window found" }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
$handled = $false
if ([ScreenCapture]::IsIconic($hwnd)) {
[ScreenCapture]::ShowWindow($hwnd, [ScreenCapture]::SW_SHOWNOACTIVATE)
Start-Sleep -Milliseconds 100
}
# 优先使用 COM 自动化(不需要把窗口切到前台)
try {
Add-Type -AssemblyName System.Runtime.InteropServices -ErrorAction SilentlyContinue | Out-Null
} catch {}
$app = $null
$progIds = @()
if ($procName -eq "POWERPNT") {
$progIds = @("PowerPoint.Application")
} else {
# WPS 兼容:常见 ProgID 尝试
$progIds = @("KWPP.Application", "WPP.Application", "PowerPoint.Application")
}
foreach ($progId in $progIds) {
try {
$app = [Runtime.InteropServices.Marshal]::GetActiveObject($progId)
if ($app) { break }
} catch {}
}
if ($app) {
try {
$dir = if ($action -eq "prev") { "prev" } else { "next" }
$sswCount = 0
try { $sswCount = $app.SlideShowWindows.Count } catch { $sswCount = 0 }
if ($sswCount -gt 0) {
$view = $app.SlideShowWindows.Item(1).View
if ($action -eq "prev") {
$view.Previous()
$handled = $true
} elseif ($action -eq "next") {
$view.Next()
$handled = $true
} elseif ($action -eq "slideshow_end") {
$view.Exit()
$handled = $true
}
} else {
if ($action -eq "slideshow_start") {
try {
$pres = $null
try { $pres = $app.ActivePresentation } catch { $pres = $null }
if ($pres) {
try { $pres.SlideShowSettings.ShowType = 2 } catch {}
$pres.SlideShowSettings.Run() | Out-Null
Start-Sleep -Milliseconds 200
try {
$sswCount2 = 0
try { $sswCount2 = $app.SlideShowWindows.Count } catch { $sswCount2 = 0 }
if ($sswCount2 -gt 0) {
$sswHwnd = [IntPtr]$app.SlideShowWindows.Item(1).HWND
if ($sswHwnd -ne [IntPtr]::Zero) {
[ScreenCapture]::SetNotTopMost($sswHwnd)
}
}
} catch {}
$handled = $true
}
} catch {}
} else {
$pres = $null
try { $pres = $app.ActivePresentation } catch { $pres = $null }
if ($pres) {
$total = 0
try { $total = $pres.Slides.Count } catch { $total = 0 }
$idx = 0
try { $idx = $app.ActiveWindow.View.Slide.SlideIndex } catch { $idx = 0 }
if ($idx -gt 0 -and $total -gt 0) {
$target = if ($action -eq "prev") { [Math]::Max(1, $idx - 1) } else { [Math]::Min($total, $idx + 1) }
try { $app.ActiveWindow.View.GotoSlide($target) } catch {}
$handled = $true
}
}
}
}
} catch {}
}
if (-not $handled) {
if ($action -eq "slideshow_start") {
[ScreenCapture]::SendKeyToWindow($hwnd, 0x74)
Start-Sleep -Milliseconds 300
try {
$ssHwnd = [IntPtr]::Zero
$hwnds2 = [ScreenCapture]::GetProcessWindows($targetProc.Id)
foreach ($h2 in $hwnds2) {
$t2 = [ScreenCapture]::GetWindowTextStr($h2)
if ($t2 -match "幻灯片放映" -or $t2 -match "Slide Show") {
$ssHwnd = $h2
break
}
}
if ($ssHwnd -ne [IntPtr]::Zero) {
[ScreenCapture]::SetNotTopMost($ssHwnd)
}
} catch {}
$handled = $true
} elseif ($action -eq "slideshow_end") {
[ScreenCapture]::SendKeyToWindow($hwnd, 0x1B)
$handled = $true
} elseif ($action -eq "prev") {
[ScreenCapture]::SendKeyToWindow($hwnd, [ScreenCapture]::VK_LEFT)
$handled = $true
} elseif ($action -eq "next") {
[ScreenCapture]::SendKeyToWindow($hwnd, [ScreenCapture]::VK_RIGHT)
$handled = $true
}
}
$respObj = @{ success = $handled }
} else {
$respObj = @{ success = $false; error = "No PPT process found" }
}
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
} catch {
$respObj = @{ success = $false; error = $_.Exception.Message }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 500
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
}
continue
}
if ($path.StartsWith("/api/local-file")) {
# Log API Request
$time = Get-Date -Format "HH:mm:ss"
Write-Host "[$time] " -NoNewline -ForegroundColor DarkGray
Write-Host "API " -NoNewline -ForegroundColor Cyan
Write-Host "- $path" -ForegroundColor Gray
$baseDir = Split-Path $root -Parent
if ([string]::IsNullOrEmpty($baseDir) -or -not (Test-Path $baseDir)) {
$baseDir = $currentDir
}
$dataDir = Join-Path $baseDir "data"
if (-not (Test-Path $dataDir)) {
New-Item -ItemType Directory -Path $dataDir -Force | Out-Null
}
$response.AddHeader("Access-Control-Allow-Origin", "*")
$response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
$response.AddHeader("Access-Control-Allow-Headers", "Content-Type")
$response.AddHeader("Cache-Control", "no-store")
if ($request.HttpMethod -eq "OPTIONS") {
$response.StatusCode = 204
$response.Close()
continue
}
$subPath = $path.Substring("/api/local-file".Length)
try {
if ($subPath -eq "/save" -and $request.HttpMethod -eq "POST") {
$ms = New-Object System.IO.MemoryStream
$request.InputStream.CopyTo($ms)
$bodyBytes = $ms.ToArray()
$ms.Dispose()
$body = [System.Text.Encoding]::UTF8.GetString($bodyBytes)
$body = $body.Trim([char]65279).Trim([char]0).Trim()
$payload = $body | ConvertFrom-Json
$key = $payload.key
$data = $payload.data
if ([string]::IsNullOrEmpty($key)) {
throw "Missing key"
}
$safeKey = ($key -replace '[^a-zA-Z0-9_-]', '_')
$filePath = Join-Path $dataDir ($safeKey + ".json")
# Fix: Use ConvertTo-Json with sufficient depth and prevent UTF-16 issues
# PowerShell 5.1 ConvertTo-Json defaults to depth 2, which breaks nested objects
$jsonToWrite = $data | ConvertTo-Json -Depth 100 -Compress
# Force UTF-8 encoding (no BOM)
[System.IO.File]::WriteAllText($filePath, $jsonToWrite, [System.Text.Encoding]::UTF8)
$respObj = @{ success = $true }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
if ($subPath -eq "/load" -and $request.HttpMethod -eq "GET") {
$key = $request.QueryString["key"]
if ([string]::IsNullOrEmpty($key)) {
throw "Missing key"
}
$safeKey = ($key -replace '[^a-zA-Z0-9_-]', '_')
$filePath = Join-Path $dataDir ($safeKey + ".json")
if (-not (Test-Path $filePath -PathType Leaf)) {
$respObj = @{ success = $false; error = "Not found" }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 404
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
$raw = Get-Content -Path $filePath -Raw -Encoding UTF8
$dataObj = $raw | ConvertFrom-Json
$respObj = @{ success = $true; data = $dataObj }
$respJson = $respObj | ConvertTo-Json -Depth 100 -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
if ($subPath -eq "/keys" -and $request.HttpMethod -eq "GET") {
$files = Get-ChildItem -Path $dataDir -Filter "*.json" -File -ErrorAction SilentlyContinue
$keysList = @()
if ($files) {
foreach ($f in $files) {
$keysList += '"' + $f.BaseName + '"'
}
}
$keysJson = "[" + ($keysList -join ",") + "]"
# Manually construct JSON to avoid PowerShell array flattening issues
$respJson = '{"success":true,"keys":' + $keysJson + '}'
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 200
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
$respObj = @{ success = $false; error = "Not found" }
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 404
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
} catch {
Write-Host "API Error: $($_.Exception.Message)" -ForegroundColor Red
$respObj = @{ success = $false; error = $_.Exception.Message }
if ([string]::IsNullOrEmpty($respObj.error)) {
$respObj.error = "$_"
}
$respJson = $respObj | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($respJson)
$response.StatusCode = 500
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $bytes.Length
$response.OutputStream.Write($bytes, 0, $bytes.Length)
$response.Close()
continue
}
}
# Default to index.html
if ($path -eq "/" -or $path -eq "") {
$path = "/index.html"
}
# Security Check
$relativePath = $path.TrimStart("/").Replace("/", "\")
$filePath = Join-Path $root $relativePath
if (-not $filePath.StartsWith($root)) {
$response.StatusCode = 403
$response.Close()
continue
}
# SPA Fallback
if (-not (Test-Path $filePath -PathType Leaf)) {
$extension = [System.IO.Path]::GetExtension($filePath)
if ([string]::IsNullOrEmpty($extension)) {
$filePath = Join-Path $root "index.html"
}
}
# Serve File
if (Test-Path $filePath -PathType Leaf) {
try {
$time = Get-Date -Format "HH:mm:ss"
# Log success (Gray time, Green status, Gray path)
Write-Host "[$time] " -NoNewline -ForegroundColor DarkGray
Write-Host "200 OK " -NoNewline -ForegroundColor Green
Write-Host "- $path" -ForegroundColor Gray
$content = [System.IO.File]::ReadAllBytes($filePath)
$ext = [System.IO.Path]::GetExtension($filePath).ToLower()
$contentType = "application/octet-stream"
if ($mimeTypes.ContainsKey($ext)) { $contentType = $mimeTypes[$ext] }
$response.ContentType = $contentType
$response.ContentLength64 = $content.Length
$response.StatusCode = 200
# Add Cache-Control headers to prevent caching issues
$servedFileName = [System.IO.Path]::GetFileName($filePath).ToLower()
if ($servedFileName -eq "sw.js" -or $servedFileName -eq "index.html") {
$response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate")
$response.AddHeader("Pragma", "no-cache")
$response.AddHeader("Expires", "0")
} else {
$response.AddHeader("Cache-Control", "public, max-age=3600")
}
$response.OutputStream.Write($content, 0, $content.Length)
} catch {
$response.StatusCode = 500
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
# Log 404 (Gray time, Yellow status, Gray path)
$time = Get-Date -Format "HH:mm:ss"
Write-Host "[$time] " -NoNewline -ForegroundColor DarkGray
Write-Host "404 Not Found " -NoNewline -ForegroundColor Yellow
Write-Host "- $path" -ForegroundColor Gray
$response.StatusCode = 404
$msg = [System.Text.Encoding]::UTF8.GetBytes("404 Not Found")
$response.ContentType = "text/plain; charset=utf-8"
$response.ContentLength64 = $msg.Length
$response.OutputStream.Write($msg, 0, $msg.Length)
}
$response.Close()
} catch {
# Ignore loop errors
}
}