卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
This commit is contained in:
commit
01a44cd288
408 files changed
+308986
No files matched your search
@@ -0,0 +1,173 @@
|
||||
<#
|
||||
卫戍协议:盟约 · 开机自动在后台运行服务器(Windows 任务计划程序)+ 防火墙规则。文档:docs\DEPLOY.md
|
||||
用法(会自动请求管理员权限):
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 # 安装并立即启动
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Port 8080 -Verify sample
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Status # 查看状态
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Restart # 更新代码后重启
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Stop
|
||||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Uninstall # 删除任务和防火墙规则
|
||||
做的事:写入 scripts\service.env.cmd(node.exe 路径、PORT、HOST、SP_COMBAT、SP_VERIFY)→ 注册计划任务
|
||||
「StrongholdProtocol」(开机时以 SYSTEM 身份运行 scripts\run-server.cmd,无需登录;进程退出 5 秒后自动重启;
|
||||
日志 logs\server.log)→ 添加入站防火墙规则「Stronghold Protocol」(TCP 端口,专用/域网络;-AllowPublicNetwork 也放行公用网络)。
|
||||
#>
|
||||
param(
|
||||
[int]$Port = 3000,
|
||||
[string]$BindHost = '0.0.0.0',
|
||||
[ValidateSet('client', 'server')][string]$Combat = 'client',
|
||||
[ValidateSet('off', 'sample', 'all')][string]$Verify = 'off',
|
||||
[string]$TaskName = 'StrongholdProtocol',
|
||||
[switch]$AllowPublicNetwork,
|
||||
[switch]$NoFirewall,
|
||||
[switch]$Status,
|
||||
[switch]$Stop,
|
||||
[switch]$Restart,
|
||||
[switch]$Uninstall
|
||||
)
|
||||
# 'Continue': native tools (netsh, node) report through exit codes; Windows PowerShell 5.1 would turn their
|
||||
# redirected stderr into terminating errors under 'Stop'. Cmdlets that must succeed use -ErrorAction Stop.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$RuleName = 'Stronghold Protocol'
|
||||
$EnvFile = Join-Path $PSScriptRoot 'service.env.cmd'
|
||||
$Runner = Join-Path $PSScriptRoot 'run-server.cmd'
|
||||
$Log = Join-Path $Root 'logs\server.log'
|
||||
|
||||
function Test-Admin {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Stop-ServerProcesses {
|
||||
# the task runs cmd.exe (run-server.cmd, a restart loop) → node.exe: stop the loop first, then its children
|
||||
$runners = @(Get-CimInstance Win32_Process -Filter "Name='cmd.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -like '*run-server.cmd*' })
|
||||
foreach ($r in $runners) {
|
||||
$children = @(Get-CimInstance Win32_Process -Filter "ParentProcessId=$($r.ProcessId)" -ErrorAction SilentlyContinue)
|
||||
Stop-Process -Id $r.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
foreach ($ch in $children) { Stop-Process -Id $ch.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Status {
|
||||
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if (-not $t) { Write-Host "计划任务「$TaskName」未安装。" -ForegroundColor Yellow }
|
||||
else {
|
||||
$info = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
Write-Host "计划任务「$TaskName」:$($t.State),上次运行 $($info.LastRunTime),结果 $($info.LastTaskResult)"
|
||||
}
|
||||
$p = $Port
|
||||
if (Test-Path $EnvFile) {
|
||||
$m = Select-String -Path $EnvFile -Pattern 'set "PORT=(\d+)"' | Select-Object -First 1
|
||||
if ($m) { $p = [int]$m.Matches[0].Groups[1].Value }
|
||||
}
|
||||
try {
|
||||
$h = Invoke-RestMethod -Uri "http://127.0.0.1:$p/healthz" -TimeoutSec 3
|
||||
Write-Host "服务器正在运行:端口 $p,房间 $($h.rooms),对局 $($h.matches),连接 $($h.sockets)" -ForegroundColor Green
|
||||
} catch { Write-Host "端口 $p 上没有响应(刚启动时请等几秒;日志:$Log)" -ForegroundColor Yellow }
|
||||
& netsh advfirewall firewall show rule name="$RuleName" | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { Write-Host "防火墙规则「$RuleName」已存在。" } else { Write-Host "防火墙规则「$RuleName」不存在。" -ForegroundColor Yellow }
|
||||
if (Test-Path $Log) { Write-Host "`n日志最后 10 行($Log):"; Get-Content $Log -Tail 10 }
|
||||
}
|
||||
|
||||
# Status needs no elevation
|
||||
if ($Status) { Show-Status; exit 0 }
|
||||
|
||||
if (-not (Test-Admin)) {
|
||||
Write-Host '需要管理员权限,正在请求提升…' -ForegroundColor Yellow
|
||||
$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"")
|
||||
foreach ($kv in $PSBoundParameters.GetEnumerator()) {
|
||||
if ($kv.Value -is [System.Management.Automation.SwitchParameter]) { if ($kv.Value.IsPresent) { $argList += "-$($kv.Key)" } }
|
||||
else { $argList += @("-$($kv.Key)", "`"$($kv.Value)`"") }
|
||||
}
|
||||
try {
|
||||
$p = Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs -Wait -PassThru -ErrorAction Stop
|
||||
exit $p.ExitCode
|
||||
} catch {
|
||||
Write-Host "未获得管理员权限,已取消:$($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Uninstall) {
|
||||
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
Stop-ServerProcesses
|
||||
& netsh advfirewall firewall delete rule name="$RuleName" | Out-Null
|
||||
if (Test-Path $EnvFile) { Remove-Item $EnvFile -Force }
|
||||
Write-Host '已删除计划任务、防火墙规则和 scripts\service.env.cmd。' -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
if ($Stop -or $Restart) {
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Stop-ServerProcesses
|
||||
Write-Host '服务器已停止。'
|
||||
if ($Stop) { exit 0 }
|
||||
Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
Start-Sleep -Seconds 3
|
||||
Show-Status
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- install ------------------------------------------------------------------------------------
|
||||
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
|
||||
if (-not $nodeCmd) { throw '未找到 Node.js:请先安装(winget install OpenJS.NodeJS.LTS),然后重新打开 PowerShell。' }
|
||||
$nodeExe = $nodeCmd.Source
|
||||
$major = [int](((& $nodeExe -v) -replace '^v', '').Split('.')[0])
|
||||
if ($major -lt 18) { throw "Node.js 版本太旧(需要 18+):$(& $nodeExe -v)" }
|
||||
if ($nodeExe -like "$env:USERPROFILE*") {
|
||||
Write-Host "注意:node.exe 位于用户目录($nodeExe,nvm/便携版?)。SYSTEM 账户一般也能运行它;若服务起不来,请改用 winget / 官网安装包安装的 Node.js。" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Set-Location $Root
|
||||
Write-Host '准备运行环境(node tools\setup.mjs)…' -ForegroundColor Cyan
|
||||
& $nodeExe tools\setup.mjs --quiet
|
||||
if ($LASTEXITCODE -ne 0) { throw 'setup 失败,请先解决上面的问题(node tools\doctor.mjs 可诊断)。' }
|
||||
|
||||
@(
|
||||
'@rem Written by scripts\install-service-windows.ps1 - re-run it to change these values.',
|
||||
"set `"NODE_EXE=$nodeExe`"",
|
||||
"set `"PORT=$Port`"",
|
||||
"set `"HOST=$BindHost`"",
|
||||
"set `"SP_COMBAT=$Combat`"",
|
||||
"set `"SP_VERIFY=$Verify`""
|
||||
) | Set-Content -Path $EnvFile -Encoding Oem -ErrorAction Stop
|
||||
|
||||
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Stop-ServerProcesses
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
}
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/d /c `"`"$Runner`"`"" -WorkingDirectory $Root
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger.Delay = 'PT20S' # give the network a moment after boot
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
|
||||
-Description "Stronghold Protocol web server ($Root)" -ErrorAction Stop | Out-Null
|
||||
Write-Host "已注册计划任务「$TaskName」(开机自动启动,无需登录)。" -ForegroundColor Green
|
||||
|
||||
if (-not $NoFirewall) {
|
||||
& netsh advfirewall firewall delete rule name="$RuleName" | Out-Null
|
||||
$profiles = if ($AllowPublicNetwork) { 'private,domain,public' } else { 'private,domain' }
|
||||
& netsh advfirewall firewall add rule name="$RuleName" dir=in action=allow protocol=TCP localport=$Port profile=$profiles | Out-Null
|
||||
Write-Host "已添加防火墙入站规则「$RuleName」:TCP $Port($profiles)。" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
Start-Sleep -Seconds 4
|
||||
Show-Status
|
||||
Write-Host "`n朋友访问地址(局域网):"
|
||||
& $nodeExe tools\doctor.mjs --port $Port | Select-String -Pattern 'http://\d' | ForEach-Object { Write-Host " $($_.Line.Trim())" }
|
||||
Write-Host "`n停止:-Stop 重启:-Restart 状态:-Status 卸载:-Uninstall 日志:$Log"
|
||||
} catch {
|
||||
Write-Host "`n出错:$($_.Exception.Message)" -ForegroundColor Red
|
||||
Read-Host '按回车键关闭' | Out-Null
|
||||
exit 1
|
||||
}
|
||||
if ($Host.Name -eq 'ConsoleHost' -and -not $env:SP_NO_PAUSE) { Read-Host "`n完成。按回车键关闭" | Out-Null }
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/launch.mjs — cross-platform "prepare + start + open the browser", used by scripts/start-windows.bat,
|
||||
// scripts/start-windows.ps1 and scripts/start.sh (docs/DEPLOY.md).
|
||||
//
|
||||
// node scripts/launch.mjs [--port 3000] [--host 0.0.0.0] [--no-open] [--no-setup] [setup options…]
|
||||
//
|
||||
// 1. If our server already answers on the port, just open the browser (double-clicking twice is harmless).
|
||||
// 2. node tools/setup.mjs --quiet (dependencies, vendor libs, art download / resume, optional local extraction);
|
||||
// setup options such as --no-assets, --no-local, --local, --game <dir>, -y are passed through.
|
||||
// 3. node server/index.js (PORT / HOST from the options or the environment; SP_COMBAT / SP_VERIFY / TRUST_PROXY /
|
||||
// DEBUG are inherited), then — once /healthz answers — prints the addresses to share and opens
|
||||
// http://localhost:<port> (not with --no-open, SP_NO_BROWSER=1, or on a Linux box without a display).
|
||||
// Ctrl+C stops the server (it gets the signal from the terminal itself); the exit code is the server's.
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const IS_WIN = process.platform === 'win32';
|
||||
|
||||
if (Number(process.versions.node.split('.')[0]) < 18) {
|
||||
console.error(`Node.js ${process.versions.node} 太旧,需要 18 或更高(推荐 22 LTS):https://nodejs.org/zh-cn/download`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { c, mark } = await import('../tools/setup.mjs');
|
||||
const { probePort, classifyAddresses, KIND_LABEL } = await import('../tools/doctor.mjs');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const o = { port: Number(process.env.PORT) || 3000, host: process.env.HOST || '0.0.0.0', open: !/^(1|true|yes)$/i.test(process.env.SP_NO_BROWSER || ''), setup: true, setupArgs: [], help: false };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
const [k, v] = a.split('=');
|
||||
const val = () => (v !== undefined ? v : argv[++i]);
|
||||
if (k === '--port') o.port = Number(val()) || o.port;
|
||||
else if (k === '--host') o.host = val() || o.host;
|
||||
else if (a === '--no-open') o.open = false;
|
||||
else if (a === '--no-setup') o.setup = false;
|
||||
else if (a === '-h' || a === '--help') o.help = true;
|
||||
else if (a === '--game') o.setupArgs.push(a, argv[++i] ?? '');
|
||||
else o.setupArgs.push(a);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
function openBrowser(url) {
|
||||
try {
|
||||
let cmd; let args;
|
||||
if (IS_WIN) { cmd = 'rundll32'; args = ['url.dll,FileProtocolHandler', url]; }
|
||||
else if (process.platform === 'darwin') { cmd = 'open'; args = [url]; }
|
||||
else {
|
||||
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
||||
cmd = 'xdg-open'; args = [url];
|
||||
}
|
||||
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function printShare(port) {
|
||||
const addrs = classifyAddresses().filter((a) => a.kind === 'lan' || a.kind === 'vpn' || a.kind === 'public');
|
||||
const line = c.dim('─'.repeat(56));
|
||||
console.log(`\n${line}`);
|
||||
console.log(`${mark.ok} ${c.bold('服务器已启动')} 本机打开:${c.cyan(`http://localhost:${port}`)}`);
|
||||
if (addrs.length) {
|
||||
console.log(' 发给朋友(需要能访问这台电脑的网络):');
|
||||
for (const a of addrs.slice(0, 4)) console.log(` ${c.cyan(`http://${a.address}:${port}`)} ${c.dim(KIND_LABEL[a.kind])}`);
|
||||
} else {
|
||||
console.log(c.warn(' 没有检测到局域网地址:朋友暂时无法连接(检查网线/Wi-Fi)。'));
|
||||
}
|
||||
console.log(c.dim(' 建房后把 4 位「同盟密钥」或「复制链接」(…/?room=密钥)发给朋友。'));
|
||||
console.log(c.dim(' 朋友打不开?运行 node tools/doctor.mjs 检查防火墙。按 Ctrl+C 停止服务器。'));
|
||||
console.log(`${line}\n`);
|
||||
}
|
||||
|
||||
async function waitHealthy(port, child, timeoutMs = 30000) {
|
||||
const until = Date.now() + timeoutMs;
|
||||
while (Date.now() < until && child.exitCode === null) {
|
||||
const p = await probePort(port);
|
||||
if (p.state === 'ours') return true;
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const o = parseArgs(process.argv.slice(2));
|
||||
if (o.help) {
|
||||
const src = fs.readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n');
|
||||
console.log(src.slice(1, 13).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
|
||||
return 0;
|
||||
}
|
||||
const localUrl = `http://localhost:${o.port}`;
|
||||
|
||||
const before = await probePort(o.port, o.host);
|
||||
if (before.state === 'ours') {
|
||||
console.log(`${mark.ok} 服务器已经在运行(端口 ${o.port}),直接打开浏览器。`);
|
||||
printShare(o.port);
|
||||
if (o.open) openBrowser(localUrl);
|
||||
return 0;
|
||||
}
|
||||
if (before.state !== 'free') {
|
||||
console.error(`${mark.err} 端口 ${o.port} 被其他程序占用或无权限(${before.code || before.state})。`);
|
||||
console.error(` 换一个端口:${IS_WIN ? 'scripts\\start-windows.bat --port 3001' : 'scripts/start.sh --port 3001'}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (o.setup) {
|
||||
const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'setup.mjs'), '--quiet', ...o.setupArgs], { cwd: ROOT, stdio: 'inherit' });
|
||||
if (r.status !== 0) { console.error(`${mark.err} 准备步骤失败(见上方)。`); return r.status || 1; }
|
||||
}
|
||||
|
||||
const env = { ...process.env, PORT: String(o.port), HOST: o.host };
|
||||
const child = spawn(process.execPath, [path.join(ROOT, 'server', 'index.js')], { cwd: ROOT, env, stdio: 'inherit' });
|
||||
const forward = (sig) => { if (child.exitCode === null) { try { child.kill(sig); } catch { /* gone */ } } };
|
||||
// SIGINT reaches the server straight from the terminal (same process group / console); forwarding it too would make
|
||||
// the server's second-signal path force-exit. Other signals (service managers, `kill`) are forwarded.
|
||||
process.on('SIGINT', () => {});
|
||||
for (const sig of ['SIGTERM', 'SIGHUP', 'SIGBREAK']) { try { process.on(sig, () => forward(sig === 'SIGBREAK' ? 'SIGTERM' : sig)); } catch { /* unsupported here */ } }
|
||||
|
||||
const exited = new Promise((resolve) => child.on('exit', (code, signal) => resolve(code ?? (signal ? 0 : 1))));
|
||||
if (await waitHealthy(o.port, child)) {
|
||||
printShare(o.port);
|
||||
if (o.open && !openBrowser(localUrl)) console.log(c.dim(`(未能自动打开浏览器,请手动访问 ${localUrl})`));
|
||||
}
|
||||
return exited;
|
||||
}
|
||||
|
||||
main().then((code) => { process.exitCode = code; }, (e) => { console.error(e?.stack || e); process.exitCode = 1; });
|
||||
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
rem Runs the server in the background for the scheduled task created by scripts\install-service-windows.ps1.
|
||||
rem Settings come from scripts\service.env.cmd (written by the installer; NODE_EXE, PORT, HOST, SP_COMBAT, SP_VERIFY).
|
||||
rem Output goes to logs\server.log (rotated at 10 MB to logs\server.old.log); the server is restarted 5 s after it exits.
|
||||
setlocal EnableExtensions
|
||||
cd /d "%~dp0.."
|
||||
set "NODE_EXE=node"
|
||||
if exist "%~dp0service.env.cmd" call "%~dp0service.env.cmd"
|
||||
if not exist "logs" mkdir "logs"
|
||||
|
||||
:loop
|
||||
if exist "logs\server.log" for %%A in ("logs\server.log") do if %%~zA GTR 10485760 move /y "logs\server.log" "logs\server.old.log" >nul
|
||||
echo [%date% %time%] starting: "%NODE_EXE%" server\index.js (PORT=%PORT% HOST=%HOST%) >> "logs\server.log"
|
||||
"%NODE_EXE%" server\index.js >> "logs\server.log" 2>&1
|
||||
echo [%date% %time%] server exited with code %errorlevel%, restarting in 5 s >> "logs\server.log"
|
||||
ping -n 6 127.0.0.1 >nul
|
||||
goto loop
|
||||
@@ -0,0 +1,53 @@
|
||||
@echo off
|
||||
rem Stronghold Protocol - double-click to start (Windows). Docs: docs\DEPLOY.md
|
||||
rem Checks Node.js, installs dependencies on the first run, runs tools\setup.mjs (art download / resume),
|
||||
rem starts the server, prints the LAN addresses and opens the browser. Extra arguments are passed to
|
||||
rem scripts\launch.mjs, e.g.: start-windows.bat --port 3001 --no-local
|
||||
chcp 65001 >nul
|
||||
setlocal EnableExtensions
|
||||
title 卫戍协议:盟约 - Stronghold Protocol
|
||||
cd /d "%~dp0.."
|
||||
|
||||
where node >nul 2>nul
|
||||
if errorlevel 1 goto :nonode
|
||||
node -e "process.exit(Number(process.versions.node.split('.')[0])>=18?0:1)"
|
||||
if errorlevel 1 goto :oldnode
|
||||
|
||||
if not exist "node_modules\ws\package.json" (
|
||||
echo [首次运行] 正在安装依赖 npm ci ...
|
||||
call npm ci --no-audit --no-fund || call npm install --no-audit --no-fund
|
||||
if errorlevel 1 goto :fail
|
||||
)
|
||||
|
||||
node scripts\launch.mjs %*
|
||||
if errorlevel 1 goto :fail
|
||||
exit /b 0
|
||||
|
||||
:nonode
|
||||
echo.
|
||||
echo 未找到 Node.js(需要 18 或更高,推荐 22 LTS)。Node.js not found.
|
||||
echo.
|
||||
echo 方法一:在 PowerShell 或命令提示符中运行
|
||||
echo winget install OpenJS.NodeJS.LTS
|
||||
echo 方法二:从官网下载安装包 https://nodejs.org/zh-cn/download
|
||||
echo.
|
||||
echo 安装完成后请关闭本窗口,再重新双击 start-windows.bat。
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:oldnode
|
||||
echo.
|
||||
for /f "delims=" %%v in ('node -v') do echo 当前 Node.js 版本 %%v 太旧,需要 18 或更高(推荐 22 LTS)。
|
||||
echo 升级:winget upgrade OpenJS.NodeJS.LTS 或 https://nodejs.org/zh-cn/download
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo 启动失败,请查看上面的错误信息。诊断:node tools\doctor.mjs
|
||||
echo Start failed - see the messages above. Diagnose with: node tools\doctor.mjs
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -0,0 +1,73 @@
|
||||
<#
|
||||
卫戍协议:盟约 · Windows 启动脚本(PowerShell 版)。文档:docs\DEPLOY.md
|
||||
运行:右键 →「使用 PowerShell 运行」,或
|
||||
powershell -ExecutionPolicy Bypass -File scripts\start-windows.ps1 [-Port 3001] [launch.mjs 的其他参数]
|
||||
检查 Node.js(没有时可用 winget 安装)→ 首次运行 npm ci → tools\setup.mjs → 启动服务器并打开浏览器。
|
||||
#>
|
||||
# PositionalBinding off: a bare launch.mjs option (.\start-windows.ps1 --no-local) must land in $Rest, not in [int]$Port.
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[int]$Port = 0,
|
||||
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Rest = @()
|
||||
)
|
||||
$ErrorActionPreference = 'Continue' # native tools report through $LASTEXITCODE
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $Root
|
||||
$Host.UI.RawUI.WindowTitle = '卫戍协议:盟约 - Stronghold Protocol'
|
||||
|
||||
function Pause-Exit([int]$code) {
|
||||
Write-Host ''
|
||||
Read-Host '按回车键关闭窗口 / Press Enter to close' | Out-Null
|
||||
exit $code
|
||||
}
|
||||
|
||||
function Test-Node {
|
||||
$cmd = Get-Command node -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) { return $null }
|
||||
$v = (& node -v) -replace '^v', ''
|
||||
return [pscustomobject]@{ Path = $cmd.Source; Version = $v; Major = [int]($v.Split('.')[0]) }
|
||||
}
|
||||
|
||||
$node = Test-Node
|
||||
if (-not $node) {
|
||||
Write-Host '未找到 Node.js(需要 18 或更高,推荐 22 LTS)。' -ForegroundColor Yellow
|
||||
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if ($winget) {
|
||||
Write-Host '可以用 Windows 自带的 winget 安装: winget install OpenJS.NodeJS.LTS'
|
||||
$ans = Read-Host '现在安装吗?[Y/n]'
|
||||
if ($ans -eq '' -or $ans -match '^(y|yes|是)$') {
|
||||
& winget install --id OpenJS.NodeJS.LTS -e --accept-source-agreements --accept-package-agreements
|
||||
$env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$node = Test-Node
|
||||
}
|
||||
} else {
|
||||
Write-Host '请从官网下载安装:https://nodejs.org/zh-cn/download'
|
||||
}
|
||||
if (-not $node) {
|
||||
Write-Host '安装完成后请重新打开本脚本(新窗口才能读到新的 PATH)。'
|
||||
Pause-Exit 1
|
||||
}
|
||||
}
|
||||
if ($node.Major -lt 18) {
|
||||
Write-Host "Node.js $($node.Version) 太旧,需要 18 或更高:winget upgrade OpenJS.NodeJS.LTS 或 https://nodejs.org/zh-cn/download" -ForegroundColor Red
|
||||
Pause-Exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path (Join-Path $Root 'node_modules\ws\package.json'))) {
|
||||
Write-Host '[首次运行] 正在安装依赖 npm ci ...' -ForegroundColor Cyan
|
||||
& npm.cmd ci --no-audit --no-fund
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
& npm.cmd install --no-audit --no-fund
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host 'npm 安装依赖失败(网络?)。' -ForegroundColor Red; Pause-Exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
$launchArgs = @('scripts\launch.mjs')
|
||||
if ($Port -gt 0) { $launchArgs += @('--port', "$Port") }
|
||||
if ($Rest) { $launchArgs += $Rest }
|
||||
& node @launchArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "`n启动失败,请查看上面的错误信息。诊断:node tools\doctor.mjs" -ForegroundColor Red
|
||||
Pause-Exit $LASTEXITCODE
|
||||
}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# 卫戍协议:盟约 · macOS / Linux start script. Docs: docs/DEPLOY.md
|
||||
# scripts/start.sh [--port 3001] [--no-open] [--no-local] [--no-assets] … (arguments go to scripts/launch.mjs)
|
||||
# Checks Node.js ≥ 18, runs `npm ci` on the first run, then scripts/launch.mjs (tools/setup.mjs → server → browser).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "未找到 Node.js(需要 18 或更高,推荐 22 LTS)。Node.js not found."
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
echo " 安装:brew install node@22 或 https://nodejs.org/zh-cn/download"
|
||||
else
|
||||
echo " 安装:https://nodejs.org/zh-cn/download (或发行版的包管理器 / nvm / fnm)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
if ! node -e "process.exit(Number(process.versions.node.split('.')[0])>=18?0:1)"; then
|
||||
echo "Node.js $(node -v) 太旧,需要 18 或更高(推荐 22 LTS):https://nodejs.org/zh-cn/download"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f node_modules/ws/package.json ]; then
|
||||
echo "[首次运行] 正在安装依赖 npm ci …"
|
||||
npm ci --no-audit --no-fund || npm install --no-audit --no-fund
|
||||
fi
|
||||
|
||||
exec node scripts/launch.mjs "$@"
|
||||
Reference in new issue
Block a user