首次提交
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user