需求 1:管理端端口与软件 API 端口分离 - 软件 API 端口 PORT(默认 3000)只提供 /api、/health、/static、/contracts, 不提供管理页面(未命中路径 404);管理端端口 ADMIN_PORT(默认 3001) 提供管理页面与全部接口;ADMIN_HOST 可单独限制管理端来源 - main.ts 同进程监听两个端口,用 req.socket.localPort 区分端口行为; ADMIN_PORT=PORT 为单端口兼容模式(只创建一个 server,无 EADDRINUSE) - 端口非法在生产环境中止启动;ADMIN_PORT=PORT 只 warn - 启动日志分别打印两个端口的访问地址 需求 2:动作数据支持 VMD(MikuMikuDance) - vmd-parser.ts:二进制解析(30 字节头 + Shift-JIS 模型名 + 111 字节/条骨骼 关键帧 + morph 段)、MMD 贝塞尔插值、GBK 模型名回退、固定 30fps - mmd-skeleton.ts:标准 MMD 骨架;静止比例从参考 PMX 骨骼段实测得到 (足首 1.43 / 膝 6.57 / 髋 11.50 / 首 16.21 / 頭 17.16,腿长 10.09) - mmd-pose.ts:FK + 两骨骼解析腿部 IK(膝盖朝正面,不可达收敛为伸直) + 左手系→右手系 Z 镜像(由真实数据对照实验确定:94.1% vs 5.9%) - retarget.ts:抽出 worldFramesToKeypointFrames(),BVH 与 VMD 共用重定向 - MotionFormat 增 vmd、扩展名映射增 .vmd;管理端上传支持 .vmd - scripts/inspect-vmd.mjs:上传前自查工具 契约:rest-api / data-models / model-formats → 0.2.0(另两库已同步逐字节一致) 验证:85 项单测通过(VMD 31 项);契约与 29 条路由一致;typecheck/build 通过; 真实样本经 HTTP 端到端:霜雪千年 2901 帧(IK 腿)、恋爱循环 4604 帧(FK 腿) 均 preprocessState=done、0 NaN、0 越界(测试数据已从数据库清理)。
11 KiB
T-WEB-010 部署(Linux 业务服务,不使用 Docker,不部署反代)
- 状态:done(两次需求变更后重做)
- 依赖:T-WEB-001 ~ 009
目标
Web 服务端以一个 Linux 业务服务的形式部署运行:Node 进程由系统服务 (systemd / OpenRC / SysVinit / 后台进程)托管,同时提供 REST API 与管理端页面。 不使用 Docker,不部署 MySQL(数据库为部署方提供的现有实例), 也不部署反向代理(由另外的服务/团队负责)。
需求变更记录
- 初版按文档 §4.4 实现为 Docker Compose;用户要求改为"运行在 Linux 的网站", 于是替换为 nginx + systemd 原生站点部署,并删除
deploy/docker-compose.yml、web-server/Dockerfile、web-server/admin/Dockerfile。- 随后用户明确:"web 端仅需要业务需求,反代等工作交由另外的服务处理。 只需要给出 iis 和 nginx 反代的说明文档,不用部署反代服务。" 于是 install.sh 彻底移除 nginx 的安装与站点配置,管理端改由 Node 服务自身 托管(
ADMIN_DIR+ SPA 深链接回退),并新增deploy/proxy/只交付说明文档。- 再随后用户要求管理端口与软件 API 端口分离:新增
ADMIN_PORT(默认PORT + 1)与可选的ADMIN_HOST(默认跟随HOST)。软件 API 端口 只给客户端调接口(/api、/health、/static、/contracts,/等返回 404), 管理端端口给浏览器(管理页面 + 全部接口)。ADMIN_PORT === PORT保留为 单端口兼容模式,只 warn 不中止。install.sh 的汇总与反代说明改为两个端口。
load
DSH.md
contracts/database.md
deploy/install.sh
deploy/uninstall.sh
deploy/.env.example
deploy/systemd/opendance-api.service
deploy/proxy/README.md
部署形态
职责边界:本端只提供业务服务;反向代理由另外的服务/团队负责。
Windows/Godot 客户端、Android App ─▶ 软件 API 端口 :3000(只提供接口,无管理页面)
├── /api/ REST API
├── /health 健康检查(数据库不可用返回 503)
├── /static/ 上传文件
└── /contracts/ 契约快照
(/、/songs 等返回 404,有意设计)
浏览器 ─▶ 反向代理(外部提供,IIS / nginx / 网关…)──▶ 管理端端口 :3001
│ 整端口转发,不需要托管静态文件 ├── / 管理端页面(含 SPA 深链接回退)
│ ├── /assets/ 前端静态资源
│ └── 上面全部接口
▼
现有 MySQL / MariaDB 实例(两个端口共用同一 Node 进程)
本服务一个 Node 进程同时监听两个端口(软件 API 端口 + 管理端端口),
管理页面只在管理端口提供;反代按用途分别转发,每个端口各一条全匹配规则即可,
既不用在代理层托管静态文件,也不用配置 SPA 回退(try_files)。
deploy/install.sh 不安装、不配置任何反向代理软件,收尾时只打印反代交接指引。
交付物
| 文件 | 说明 |
|---|---|
deploy/install.sh |
一键部署:前置检查 → 建用户/目录 → 复制并校验文件 → 装依赖 → 迁移 → 注册系统服务 → 健康检查;汇总打印两个端口与防火墙放行提示 |
deploy/uninstall.sh |
卸载;默认保留上传数据,--purge 才删除 |
deploy/.env.example |
部署环境变量样例(PORT / ADMIN_PORT / ADMIN_HOST / HOST 说明与上传上限告警) |
deploy/systemd/opendance-api.service |
systemd 单元,含 ProtectSystem=strict 等安全加固 |
deploy/openrc/opendance-api |
OpenRC 服务脚本 |
deploy/initd/opendance-api |
SysVinit / LSB 启动脚本(转调 ctl.sh) |
deploy/ctl.sh |
无 init 系统时的进程控制(后台进程 + PID 文件),start/stop/restart/status/logs |
deploy/proxy/README.md |
反代说明文档:转发目标、绑定地址、自检清单、故障对照 |
deploy/proxy/nginx.conf.example |
nginx 配置样例(上传上限、超时、keepalive、可选 HTTPS) |
deploy/proxy/web.config.example |
IIS 配置样例(需 ARR + URL Rewrite 模块) |
进程管理:四层降级
"运行在 Linux 上"不应等于"必须有 systemd"。安装时按以下顺序探测并选择:
1. systemd /run/systemd/system 存在且 systemctl 可用
2. OpenRC rc-service + rc-update 可用
3. SysVinit /etc/init.d 存在且 update-rc.d 或 chkconfig 可用
4. 都没有 nohup 后台进程 + PID 文件(deploy/ctl.sh)
并写 crontab @reboot 实现开机自启
三种 init 方式最终都调用同一个 Node 进程,只是守护方式不同;第 4 种由
deploy/ctl.sh 提供 start/stop/restart/status/logs。
(注意:nohup 不能调用 shell 函数,故 ctl.sh 用
spawn_service_process() 拼装外部命令,而非包装函数。)
不使用 sudo:脚本已要求 root,降权到服务用户用 runuser → setpriv → su
依次回退(部分服务器根本没装 sudo)。
关键实现
- 双端口:一个 Node 进程创建两个 HTTP server——软件 API 端口(
PORT,默认 3000) 与管理端端口(ADMIN_PORT,默认 3001),复用同一个 express 实例;用req.socket.localPort判断请求来自哪个端口,决定是否提供管理页面。ADMIN_PORT === PORT时只建一个 server(单端口兼容模式,避免 EADDRINUSE) - 服务自身托管管理端:
app.useStaticAssets(adminDir, …)+ SPA 回退中间件 (仅管理端口生效);回退会跳过/api/、/health、/static/、/contracts/以及带扩展名的路径,保证未知 API 仍返回 404 而不是 HTML - 软件 API 端口不提供管理页面:
/、/songs等在该端口一律 404(有意设计) HOST/ADMIN_HOST绑定地址:同机反代建议127.0.0.1,异机反代用0.0.0.0+ 防火墙;ADMIN_HOST可选(默认跟随HOST),可单独把管理端限制在 本机或内网;HOST默认取0.0.0.0以保证开箱可用- 端口校验:
PORT/ADMIN_PORT必须是 1..65535 的整数;非法值在生产环境 启动时快速失败(validateEnv的 errors),ADMIN_PORT === PORT只 warn - install.sh 汇总与反代说明:结尾分别打印两个端口的访问地址、监听配置与
防火墙放行命令(
firewall-cmd/ufw同时放行两个端口) - systemd 加固:
NoNewPrivileges、ProtectSystem=strict、ProtectHome、ReadWritePaths=/var/lib/opendance,进程只能写自己的存储目录 - 上传上限两侧一致:后端
UPLOAD_MAX_BYTES=536870912(512 MiB), 反代侧client_max_body_size/maxAllowedContentLength必须不小于此值 - 迁移用编译产物:
web-server/scripts/migrate.mjs检测到dist/时走typeorm -d dist/db/data-source.js,无需在服务器上装 devDependencies - 依赖安装显式带 dev:安装脚本先
set -a; . deploy/.env导出了NODE_ENV=production,会让 npm 跳过 devDependencies 导致tsc: not found; 故用env -u NODE_ENV npm ci --include=dev - 复制文件时锚定排除:
storage/这类模式会误伤src/storage/与dist/storage/(曾导致MODULE_NOT_FOUND …storage.module),排除规则一律用/storage/锚定 /contracts/静态暴露:另两端可直接从站点取契约快照
验收
cp deploy/.env.example deploy/.env # 填写 DB_*
bash deploy/install.sh # 需 root(不需要 sudo)
curl -fsS http://localhost:3000/health
curl -fsS http://localhost:3001/health
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/ # 期望 404(软件 API 端口无管理页面)
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/ # 期望 200(管理页面)
| 验收项 | 状态 |
|---|---|
deploy/install.sh 脚本语法与幂等性 |
✅ bash -n 通过;重复执行会覆盖程序文件、保留 .env |
| 连接现有 MySQL 成功 | ✅ 已用真实实例验证(/health 200,database.connected=true) |
| TypeORM migration 执行成功 | ✅ 5 张业务表已创建并验证 |
curl http://<host>:3000/health 通过 |
✅ 直连后端端口已验证 |
软件 API 端口 / 返回 404 |
✅ PORT=3000 上 /、/songs 均 404(有意设计,无管理页面) |
管理端端口 / 返回 200 |
✅ ADMIN_PORT=3001 上 /、/songs、/models 均返回同一份 HTML(200) |
| 前端深链接回退 | ✅ 未知静态路径回退到 index.html,未知 API 仍 404 |
ADMIN_PORT === PORT 单端口兼容模式 |
✅ 只建一个 server,管理页面回到 API 端口,日志提示「未分端口」 |
| 端口非法在生产环境中止启动 | ✅ validateEnv 对非 1..65535 整数端口报 error |
| 不部署、不管理 MySQL | ✅ 部署脚本不含任何数据库安装/建库操作 |
| 不使用 Docker | ✅ 仓库内已无 Dockerfile / docker-compose / Docker 引用 |
| 不部署反向代理 | ✅ 仅交付 deploy/proxy/ 文档,install.sh 中无任何反代安装动作 |
环境限制说明:本次执行环境无 root 权限,且未安装 nginx(按需求也不应安装)。 因此
deploy/install.sh的服务注册链路(systemd / OpenRC / SysVinit / crontab) 未能在本机实机跑通。已完成的等价验证:
- 后端以
node dist/main.js直连真实数据库启动,两个端口/health均 200- 管理端托管与 SPA 回退逐路径实测(
/、/songs、/models、/api/*、未知路径), 并确认 3000 上返回 404、3001 上返回 200ADMIN_DIR环境变量生效,index.html缺失时打印告警而非崩溃npm run migration:run走编译产物成功- 端到端冒烟 57 项全通过
install.sh/uninstall.sh/ctl.sh通过bash -n语法校验- 反代文档中的占位符与后端实际行为逐条对照(两个端口、路径、上传上限、健康检查)
需在一台有 root 的 Linux 服务器上补做一次
install.sh实机验收。
发布产物
./scripts/package-release.sh
# → dist-release/opendance-web-v0.1.0.tar.gz
内容:web-server/(源码 + dist)、admin/(构建产物)、deploy/
(含 proxy/ 反代文档)、contracts/ 快照、THIRD_PARTY.md、README.txt。
打包脚本会对产物做硬校验:src/**/*.ts 必须都有对应 dist/*.js,
关键文件(dist/main.js、dist/storage/storage.module.js、admin/index.html、
deploy/proxy/*)必须齐全,且不得含真实 .env 与 Docker/nginx 安装配置。