@dsh-remote/relay 0.0.0-stage → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -2
- package/SECURITY.md +19 -0
- package/deploy/README.md +125 -0
- package/deploy/docker/Dockerfile +150 -0
- package/deploy/docker/README.md +192 -0
- package/deploy/docker/compose.yaml +116 -0
- package/deploy/nginx/drc.conf +69 -0
- package/deploy/systemd/dsh-remote-control.service +66 -0
- package/deploy/windows/drc-relay-install.cmd +113 -0
- package/dist/bundle/main.js +26031 -0
- package/package.json +57 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 中继的容器编排(Compose v2,生产用的就是这一份)。
|
|
2
|
+
#
|
|
3
|
+
# docker compose -f deploy/docker/compose.yaml up -d --build
|
|
4
|
+
# docker compose -f deploy/docker/compose.yaml ps # 看健康状态
|
|
5
|
+
# docker compose -f deploy/docker/compose.yaml logs -f relay
|
|
6
|
+
# docker compose -f deploy/docker/compose.yaml down
|
|
7
|
+
#
|
|
8
|
+
# ## 变量从哪来
|
|
9
|
+
#
|
|
10
|
+
# Compose 自动读**本文件所在目录**的 .env。所以 token 写在那儿,而不是命令行上
|
|
11
|
+
# (命令行会进 shell history,也会进 ps 的 argv)。.env 已被 .gitignore 忽略。
|
|
12
|
+
# 下面每一条 都是"不配就与裸机 systemd 单元同义"——
|
|
13
|
+
# 容器不该偷偷改变程序的默认行为,它只改变"怎么被托管"。
|
|
14
|
+
#
|
|
15
|
+
# ## 与裸机部署的关系:nginx 一行都不用改
|
|
16
|
+
#
|
|
17
|
+
# 端口只发布到**宿主机回环**(127.0.0.1:8787:8787),所以宿主机上的
|
|
18
|
+
# /etc/nginx/conf.d/drc.conf(proxy_pass http://127.0.0.1:8787)原样继续工作,
|
|
19
|
+
# TLS 仍然由 nginx 终止。切容器化不动证书、不动反代,只换进程托管方式。
|
|
20
|
+
name: dsh-remote-relay
|
|
21
|
+
|
|
22
|
+
# 具名卷:docker compose up 开箱即用,且属主由镜像里的 /data 决定(uid 10001)。
|
|
23
|
+
# 要用宿主机路径(生产那样做,落在 /var/lib 下)就把 DRC_DATA_DIR 指过去并 chown。
|
|
24
|
+
volumes:
|
|
25
|
+
relay-data:
|
|
26
|
+
|
|
27
|
+
services:
|
|
28
|
+
relay:
|
|
29
|
+
build:
|
|
30
|
+
context: ../..
|
|
31
|
+
dockerfile: deploy/docker/Dockerfile
|
|
32
|
+
args:
|
|
33
|
+
# 标签里的版本/提交号。CI 与发布脚本会传;本地构建不传就是 0.0.0/unknown,
|
|
34
|
+
# 而真正认版本的地方是 /healthz(产物内注入),两者由构建阶段的断言绑定。
|
|
35
|
+
DRC_VERSION: ${DRC_VERSION:-0.0.0}
|
|
36
|
+
VCS_REF: ${VCS_REF:-unknown}
|
|
37
|
+
# 标签与 /healthz 的 version 分工:标签给 docker inspect 与部署脚本,
|
|
38
|
+
# /healthz 给运维"线上跑的是哪一版"。
|
|
39
|
+
image: ${DRC_IMAGE:-dsh-remote-relay}:${DRC_IMAGE_TAG:-local}
|
|
40
|
+
|
|
41
|
+
# 崩溃即拉起,与 systemd 的 Restart=always 同义。
|
|
42
|
+
# 刻意**不是** always:中继优雅停机时会 exit 0,always 会在 docker compose down
|
|
43
|
+
# 之后又把它拉起来("停机之后进程自己回来了"是最难查的一类故障)。
|
|
44
|
+
restart: unless-stopped
|
|
45
|
+
|
|
46
|
+
ports:
|
|
47
|
+
# 只绑回环。公网走 443(宿主机 nginx),8787 不对公网。
|
|
48
|
+
- '127.0.0.1:${DRC_PORT:-8787}:${DRC_PORT:-8787}'
|
|
49
|
+
|
|
50
|
+
environment:
|
|
51
|
+
# 唯一必填项。写错/漏写时 compose 在**启动前**就报出来,而不是让容器起来
|
|
52
|
+
# 再自己拒绝启动(那时报错在 logs 里,人不一定看)。
|
|
53
|
+
DRC_HOST_TOKEN: ${DRC_HOST_TOKEN:?在 deploy/docker/.env 里设 DRC_HOST_TOKEN(openssl rand -hex 32)}
|
|
54
|
+
DRC_PORT: ${DRC_PORT:-8787}
|
|
55
|
+
DRC_BIND: '0.0.0.0'
|
|
56
|
+
DRC_PUBLIC_URL: ${DRC_PUBLIC_URL:-}
|
|
57
|
+
DRC_LOG_LEVEL: ${DRC_LOG_LEVEL:-info}
|
|
58
|
+
DRC_PAIR_TTL_MS: ${DRC_PAIR_TTL_MS:-120000}
|
|
59
|
+
# 落盘:与卷是一对,所以写在这里与下面的 volumes 并排。
|
|
60
|
+
# 默认用**具名卷**:Docker 会用镜像里 /data 的属主(uid 10001)初始化它,所以
|
|
61
|
+
# `docker compose up` 开箱即用。改成宿主机路径(bind mount)时,属主必须也是
|
|
62
|
+
# 10001,否则状态写不进去 —— 属主不对时 2.x 启动就会记一条 error;1.x 时代只能等
|
|
63
|
+
# 每 60 秒一条的 state file write failed,而配对表面完全正常(HANDOFF 0.1)。
|
|
64
|
+
DRC_STATE_FILE: ${DRC_STATE_FILE:-/data/state.json}
|
|
65
|
+
DRC_STATE_SAVE_MS: ${DRC_STATE_SAVE_MS:-60000}
|
|
66
|
+
DRC_MAX_CONNS: ${DRC_MAX_CONNS:-200}
|
|
67
|
+
DRC_CONV_IDLE_TTL_MS: ${DRC_CONV_IDLE_TTL_MS:-604800000}
|
|
68
|
+
DRC_HOST_GRACE_MS: ${DRC_HOST_GRACE_MS:-120000}
|
|
69
|
+
|
|
70
|
+
volumes:
|
|
71
|
+
- ${DRC_DATA_DIR:-relay-data}:/data
|
|
72
|
+
|
|
73
|
+
# 与 systemd 单元同款的加固,容器里没有理由更松:
|
|
74
|
+
# - read_only + tmpfs /tmp:中继只写 /data(卷),其余一律只读;
|
|
75
|
+
# - cap_drop ALL + no-new-privileges:它不需要任何 capability,也不需要提权;
|
|
76
|
+
# - mem_limit 与单元里的 MemoryMax=512M 取同一个数,两套形态口径一致。
|
|
77
|
+
read_only: true
|
|
78
|
+
tmpfs:
|
|
79
|
+
- /tmp
|
|
80
|
+
cap_drop:
|
|
81
|
+
- ALL
|
|
82
|
+
security_opt:
|
|
83
|
+
- no-new-privileges:true
|
|
84
|
+
mem_limit: 512m
|
|
85
|
+
# 每条 WebSocket 占用一个文件描述符,默认上限对 maxConnections 来说偏紧。
|
|
86
|
+
ulimits:
|
|
87
|
+
nofile:
|
|
88
|
+
soft: 4096
|
|
89
|
+
hard: 4096
|
|
90
|
+
|
|
91
|
+
# 停机窗口要大于程序内部的 5 秒兜底(main.ts 的 shutdown guard),
|
|
92
|
+
# 否则 compose 先 SIGKILL,状态文件的最后一次补写就被砍掉了。
|
|
93
|
+
stop_grace_period: 15s
|
|
94
|
+
stop_signal: SIGTERM
|
|
95
|
+
|
|
96
|
+
# 日志轮转:生产上 relay 的日志是有界的(每个字符串值截断到 256 字符,
|
|
97
|
+
# tests/log.test.mjs 有判据),但**行数**无界。不设上限的话 json-file 会一直长,
|
|
98
|
+
# 把磁盘吃满——而这正是中继所在那台机器上唯一真正稀缺的东西。
|
|
99
|
+
logging:
|
|
100
|
+
driver: json-file
|
|
101
|
+
options:
|
|
102
|
+
max-size: '10m'
|
|
103
|
+
max-file: '5'
|
|
104
|
+
|
|
105
|
+
healthcheck:
|
|
106
|
+
test:
|
|
107
|
+
[
|
|
108
|
+
'CMD',
|
|
109
|
+
'node',
|
|
110
|
+
'-e',
|
|
111
|
+
"fetch('http://127.0.0.1:'+(process.env.DRC_PORT||8787)+'/healthz').then(r=>r.json()).then(j=>process.exit(j.ok===true?0:1)).catch(()=>process.exit(1))",
|
|
112
|
+
]
|
|
113
|
+
interval: 30s
|
|
114
|
+
timeout: 5s
|
|
115
|
+
retries: 3
|
|
116
|
+
start_period: 10s
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# nginx vhost for the dsh-remote-control relay.
|
|
2
|
+
#
|
|
3
|
+
# Install to /etc/nginx/conf.d/drc.conf (conf.d is included inside the http
|
|
4
|
+
# block, so the http-level directives below are valid here too).
|
|
5
|
+
#
|
|
6
|
+
# drc.provid.cc --wss--> nginx (TLS) --ws--> 127.0.0.1:8787
|
|
7
|
+
#
|
|
8
|
+
# Certificates are issued/renewed by acme.sh with the Aliyun DNS-01 hook
|
|
9
|
+
# (see deploy/README.md). Nothing needs port 80 at renewal time.
|
|
10
|
+
|
|
11
|
+
# ── http-level directives ────────────────────────────────────────────
|
|
12
|
+
map $http_upgrade $connection_upgrade {
|
|
13
|
+
default upgrade;
|
|
14
|
+
'' close;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Per-IP throttle. The relay only has global + per-connection budgets because
|
|
18
|
+
# it cannot see the real client IP; this is where per-IP limiting belongs.
|
|
19
|
+
limit_req_zone $binary_remote_addr zone=drc:10m rate=20r/s;
|
|
20
|
+
limit_conn_zone $binary_remote_addr zone=drc_conn:10m;
|
|
21
|
+
|
|
22
|
+
# ── port 80: redirect only ───────────────────────────────────────────
|
|
23
|
+
server {
|
|
24
|
+
listen 80;
|
|
25
|
+
listen [::]:80;
|
|
26
|
+
server_name drc.provid.cc;
|
|
27
|
+
return 301 https://$host$request_uri;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# ── port 443: TLS + WebSocket reverse proxy ──────────────────────────
|
|
31
|
+
server {
|
|
32
|
+
listen 443 ssl;
|
|
33
|
+
listen [::]:443 ssl;
|
|
34
|
+
http2 on;
|
|
35
|
+
server_name drc.provid.cc;
|
|
36
|
+
|
|
37
|
+
ssl_certificate /etc/nginx/ssl/drc.provid.cc.crt;
|
|
38
|
+
ssl_certificate_key /etc/nginx/ssl/drc.provid.cc.key;
|
|
39
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
40
|
+
ssl_prefer_server_ciphers off;
|
|
41
|
+
ssl_session_cache shared:SSL:10m;
|
|
42
|
+
ssl_session_timeout 1d;
|
|
43
|
+
|
|
44
|
+
add_header Strict-Transport-Security "max-age=31536000" always;
|
|
45
|
+
add_header X-Content-Type-Options nosniff always;
|
|
46
|
+
|
|
47
|
+
access_log /var/log/nginx/drc.access.log main;
|
|
48
|
+
error_log /var/log/nginx/drc.error.log warn;
|
|
49
|
+
|
|
50
|
+
# WebSocket upgrade + long-lived connection
|
|
51
|
+
location / {
|
|
52
|
+
limit_req zone=drc burst=40 nodelay;
|
|
53
|
+
limit_conn drc_conn 8;
|
|
54
|
+
|
|
55
|
+
proxy_pass http://127.0.0.1:8787;
|
|
56
|
+
proxy_http_version 1.1;
|
|
57
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
58
|
+
proxy_set_header Connection $connection_upgrade;
|
|
59
|
+
proxy_set_header Host $host;
|
|
60
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
61
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
62
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
63
|
+
|
|
64
|
+
# the relay is a long-lived socket: never time it out mid-session
|
|
65
|
+
proxy_read_timeout 3600s;
|
|
66
|
+
proxy_send_timeout 3600s;
|
|
67
|
+
proxy_buffering off;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# systemd unit for the dsh-remote-control relay.
|
|
2
|
+
#
|
|
3
|
+
# /etc/systemd/system/dsh-remote-control.service
|
|
4
|
+
# /etc/dsh-remote-control.env (chmod 600, DRC_HOST_TOKEN=...)
|
|
5
|
+
#
|
|
6
|
+
# The relay binds 127.0.0.1 by default — nginx terminates TLS and proxies wss
|
|
7
|
+
# to it. (Override with DRC_BIND in the env file if it must be the edge.)
|
|
8
|
+
#
|
|
9
|
+
# What actually runs is ONE self-contained file. `pnpm build`
|
|
10
|
+
# emits dist/bundle/main.js with `ws` and the protocol layer (dsh-remote-wire) inlined,
|
|
11
|
+
# so the deployment is "copy that file" — no node_modules, no `npm install --omit=dev`,
|
|
12
|
+
# and therefore no way for the server's dependency tree to drift from CI's.
|
|
13
|
+
# The old unit ran `src/main.mjs`; that entry point is gone, and `dist/src/main.js`
|
|
14
|
+
# (the unbuilt-tree form) needs devDependencies to resolve dsh-remote-wire —
|
|
15
|
+
# the bundle is the only supported production entry.
|
|
16
|
+
|
|
17
|
+
[Unit]
|
|
18
|
+
Description=DSH Remote Control relay (zero-knowledge WebSocket relay)
|
|
19
|
+
Documentation=file:///opt/dsh-remote-control/README.md
|
|
20
|
+
After=network-online.target
|
|
21
|
+
Wants=network-online.target
|
|
22
|
+
|
|
23
|
+
[Service]
|
|
24
|
+
Type=simple
|
|
25
|
+
User=root
|
|
26
|
+
WorkingDirectory=/opt/dsh-remote-control/server
|
|
27
|
+
EnvironmentFile=/etc/dsh-remote-control.env
|
|
28
|
+
ExecStart=/usr/bin/node /opt/dsh-remote-control/server/relay.mjs
|
|
29
|
+
Restart=always
|
|
30
|
+
RestartSec=3
|
|
31
|
+
|
|
32
|
+
# The relay already handles SIGTERM gracefully (drain sockets, exit 0);
|
|
33
|
+
# give it room, then be firm.
|
|
34
|
+
TimeoutStopSec=15
|
|
35
|
+
KillSignal=SIGTERM
|
|
36
|
+
|
|
37
|
+
# 会话表落盘(DRC_STATE_FILE)用的目录。
|
|
38
|
+
#
|
|
39
|
+
# 这条是 `ProtectSystem=strict` 的直接后果,不是顺手加的:strict 把整个文件系统
|
|
40
|
+
# 挂成只读(除 ReadWritePaths),所以 2026-10-06 第一次上 1.0.6 时,
|
|
41
|
+
# 状态文件写到 /var/lib/... 每 5 秒失败一次(EROFS),配对表面还在、其实**永远不落盘**。
|
|
42
|
+
# StateDirectory= 让 systemd 建目录、按 User/Group 修属主,并在 strict 下把这一个
|
|
43
|
+
# 路径放回可写——**不需要**为它放松 ProtectSystem。
|
|
44
|
+
# 别改成 ReadWritePaths=/var/lib:那是把整个 /var/lib 放开,范围大得多。
|
|
45
|
+
StateDirectory=dsh-remote-control
|
|
46
|
+
UMask=0077
|
|
47
|
+
|
|
48
|
+
# --- hardening ---
|
|
49
|
+
NoNewPrivileges=true
|
|
50
|
+
PrivateTmp=true
|
|
51
|
+
ProtectSystem=strict
|
|
52
|
+
ProtectHome=true
|
|
53
|
+
ReadWritePaths=/opt/dsh-remote-control
|
|
54
|
+
ProtectKernelTunables=true
|
|
55
|
+
ProtectKernelModules=true
|
|
56
|
+
ProtectControlGroups=true
|
|
57
|
+
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
58
|
+
MemoryMax=512M
|
|
59
|
+
|
|
60
|
+
# --- logging ---
|
|
61
|
+
StandardOutput=journal
|
|
62
|
+
StandardError=journal
|
|
63
|
+
SyslogIdentifier=dsh-remote-control
|
|
64
|
+
|
|
65
|
+
[Install]
|
|
66
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem ============================================================================
|
|
3
|
+
rem drc-relay-install.cmd — 把中继装成 Windows 服务(V3-PLAN D1 / §6.3)
|
|
4
|
+
rem
|
|
5
|
+
rem 为什么需要这个:v2 只有 Linux(systemd)与容器两条路,Windows 上没有配方。
|
|
6
|
+
rem 而"服务停止"在 Windows 上是**硬杀**——Node 收不到 SIGTERM,
|
|
7
|
+
rem 于是排空(close)与兜底落盘(forceShutdown)一次都不跑(见 §A3 / lifecycle.ts)。
|
|
8
|
+
rem 装成服务之后至少有了「停止 = 走一遍优雅停机」这条��。
|
|
9
|
+
rem
|
|
10
|
+
rem 用法(管理员 PowerShell 或 cmd):
|
|
11
|
+
rem 1) 先构建(Linux/macOS 同理):pnpm -C packages/relay build
|
|
12
|
+
rem 2) 设好环境变量(见下)
|
|
13
|
+
rem 3) drc-relay-install.cmd install
|
|
14
|
+
rem
|
|
15
|
+
rem 依赖:NSSM(把任意 exe 变成 Windows 服务)。这是**外部**依赖,不进本仓——
|
|
16
|
+
rem 它是 Windows 上没有内置 service wrapper 的事实所迫(systemd 是 Linux 自带的)。
|
|
17
|
+
rem 下载:https://nssm.cc/ (解压后把 nssm.exe 放到 PATH,或用 NSSM 环境变量指到它)
|
|
18
|
+
rem ============================================================================
|
|
19
|
+
setlocal enabledelayedexpansion
|
|
20
|
+
|
|
21
|
+
set "APP_DIR=%~dp0..\server"
|
|
22
|
+
set "SERVICE_NAME=dsh-remote-relay"
|
|
23
|
+
set "DISPLAY_NAME=DSH Remote Control Relay (zero-knowledge)"
|
|
24
|
+
set "DESCRIPTION=DSH Remote Control 的零知识中继。只做路由,不碰载荷明文。"
|
|
25
|
+
set "NSSM=%NSSM%"
|
|
26
|
+
|
|
27
|
+
if "%NSSM%"=="" set "NSSM=nssm.exe"
|
|
28
|
+
|
|
29
|
+
if not exist "%APP_DIR%\relay.mjs" (
|
|
30
|
+
echo [x] 找不到 %APP_DIR%\relay.mjs
|
|
31
|
+
echo 先构建并拷贝产物:pnpm -C packages/relay build
|
|
32
|
+
echo 然后把 dist\bundle\main.js 放到 %APP_DIR%\relay.mjs
|
|
33
|
+
exit /b 1
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
rem ── 环境变量:凭据不进命令行历史,也不进服务描述 ───────────────────────────
|
|
37
|
+
rem ⚠️ 中继**没有** hostToken 就在启动时拒绝(这是协议层纪律)。
|
|
38
|
+
rem 所以下面两行必须配好,否则服务起来又立刻退出,日志里只有 "fatal"。
|
|
39
|
+
if "%DRC_HOST_TOKEN%"=="" (
|
|
40
|
+
echo [!] 未设 DRC_HOST_TOKEN。中继缺它会**拒绝启动**——先设好再装。
|
|
41
|
+
echo 临时设:set DRC_HOST_TOKEN=... ^(openssl rand -hex 32 生成^)
|
|
42
|
+
exit /b 1
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if /i "%~1"=="install" goto :install
|
|
46
|
+
if /i "%~1"=="uninstall" goto :uninstall
|
|
47
|
+
if /i "%~1"=="status" goto :status
|
|
48
|
+
goto :usage
|
|
49
|
+
|
|
50
|
+
:install
|
|
51
|
+
echo [1/4] 注册服务 %SERVICE_NAME% ...
|
|
52
|
+
"%NSSM%" install "%SERVICE_NAME%" "%APP_DIR%\relay.mjs"
|
|
53
|
+
if errorlevel 1 goto :nssm_failed
|
|
54
|
+
|
|
55
|
+
echo [2/4] 设置 AppDirectory ...
|
|
56
|
+
"%NSSM%" set "%SERVICE_NAME%" AppDirectory "%APP_DIR%"
|
|
57
|
+
|
|
58
|
+
echo [3/4] 设置环境变量(凭据 / 端口 / 绑定)...
|
|
59
|
+
rem nssm set 会把值写进服务配置,落在注册表里 —— 所以这台机器的凭据由它的 ACL 守着。
|
|
60
|
+
rem **不要**把 hostToken 写进 deploy 文件或文档示例里。
|
|
61
|
+
"%NSSM%" set "%SERVICE_NAME%" AppEnvironmentExtra "DRC_HOST_TOKEN=%DRC_HOST_TOKEN%" "DRC_PORT=%DRC_PORT%" "DRC_BIND=127.0.0.1" "DRC_LOG_LEVEL=info"
|
|
62
|
+
if "%DRC_STATE_FILE%"=="" (
|
|
63
|
+
echo 提示:未设 DRC_STATE_FILE = 重启丢失全部配对(纯内存)。生产请设。
|
|
64
|
+
) else (
|
|
65
|
+
"%NSSM%" set "%SERVICE_NAME%" AppEnvironmentExtra "DRC_STATE_FILE=%DRC_STATE_FILE%"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
echo [4/4] 配置日志与重启策略 ...
|
|
69
|
+
rem 默认级别 info:debug 会把**完整配对码**打进日志(server.ts)。
|
|
70
|
+
"%NSSM%" set "%SERVICE_NAME%" AppStdout "%APP_DIR%\logs\relay.out.log"
|
|
71
|
+
"%NSSM%" set "%SERVICE_NAME%" AppStderr "%APP_DIR%\logs\relay.err.log"
|
|
72
|
+
"%NSSM%" set "%SERVICE_NAME%" AppRotateFiles 1
|
|
73
|
+
"%NSSM%" set "%SERVICE_NAME%" AppRotateBytes 10485760
|
|
74
|
+
rem 服务被停止时先发 Ctrl+C(= SIGINT)而不是 TerminateProcess:
|
|
75
|
+
rem 那 5 秒里排空与兜底落盘才跑得起来(lifecycle.ts 的 registerSignalHandlers)。
|
|
76
|
+
"%NSSM%" set "%SERVICE_NAME%" AppStopMethodConsole 15000
|
|
77
|
+
|
|
78
|
+
if not exist "%APP_DIR%\logs" mkdir "%APP_DIR%\logs"
|
|
79
|
+
|
|
80
|
+
echo.
|
|
81
|
+
echo ✅ 已安装为服务。常用命令:
|
|
82
|
+
echo %NSSM% start %SERVICE_NAME% 启动
|
|
83
|
+
echo %NSSM% stop %SERVICE_NAME% 停止(走优雅停机)
|
|
84
|
+
echo %NSSM% status %SERVICE_NAME% 看状态
|
|
85
|
+
echo.
|
|
86
|
+
echo ⚠️ 反代与防火墙(这一步**不能省**):
|
|
87
|
+
echo - TLS 由反代终止(IIS / nginx / Caddy)。中继只绑 127.0.0.1。
|
|
88
|
+
echo - 反代必须转发 WebSocket 升级头(Upgrade / Connection / Host)。
|
|
89
|
+
echo - 防火墙:**不要**把 8787 暴露到公网(那样就绕过了反代的按 IP 限流)。
|
|
90
|
+
exit /b 0
|
|
91
|
+
|
|
92
|
+
:uninstall
|
|
93
|
+
"%NSSM%" stop "%SERVICE_NAME%" >nul 2>&1
|
|
94
|
+
"%NSSM%" remove "%SERVICE_NAME%" confirm
|
|
95
|
+
echo ✅ 已卸载服务(%APP_DIR% 下的文件与日志保留)。
|
|
96
|
+
exit /b 0
|
|
97
|
+
|
|
98
|
+
:status
|
|
99
|
+
"%NSSM%" status "%SERVICE_NAME%"
|
|
100
|
+
exit /b 0
|
|
101
|
+
|
|
102
|
+
:nssm_failed
|
|
103
|
+
echo [x] NSSM 调用失败。检查 "%NSSM%" 是否可用(set NSSM=D:\path\to\nssm.exe)。
|
|
104
|
+
exit /b 1
|
|
105
|
+
|
|
106
|
+
:usage
|
|
107
|
+
echo 用法:
|
|
108
|
+
echo %~nx0 install 装成 Windows 服务(需要管理员)
|
|
109
|
+
echo %~nx0 status 看服务状态
|
|
110
|
+
echo %~nx0 uninstall 卸载服务
|
|
111
|
+
echo.
|
|
112
|
+
echo 前置:pnpm -C packages/relay build,并把 dist\bundle\main.js 放到 %APP_DIR%\relay.mjs
|
|
113
|
+
exit /b 1
|