卫戍协议:盟约 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:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+173
View File
@@ -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 }
+135
View File
@@ -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; });
+17
View File
@@ -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
+53
View File
@@ -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
+73
View File
@@ -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
}
+27
View File
@@ -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 "$@"