feat(web): 端口分离 + VMD 动作支持;契约升至 0.2.0

需求 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 越界(测试数据已从数据库清理)。
This commit is contained in:
2026-09-15 08:47:37 +08:00
parent 76725e5ce3
commit bba5bcf59e
35 changed files with 2658 additions and 192 deletions
+39 -14
View File
@@ -16,7 +16,8 @@
# APP_DIR 安装目录(默认 /opt/opendance)
# SERVICE_USER 运行服务的系统用户(默认 opendance)
# STORAGE_DIR 上传文件目录(默认 /var/lib/opendance/storage)
# PORT API 监听端口(默认 3000)
# PORT 软件 API 监听端口(默认 3000)
# ADMIN_PORT 管理端端口(默认 PORT + 1;设为与 PORT 相同即单端口模式)
# ============================================================
set -euo pipefail
# 失败时打印出错行号,避免只看到半截输出
@@ -29,6 +30,7 @@ APP_DIR="${APP_DIR:-/opt/opendance}"
SERVICE_USER="${SERVICE_USER:-opendance}"
STORAGE_DIR="${STORAGE_DIR:-/var/lib/opendance/storage}"
API_PORT="${PORT:-3000}"
ADMIN_PORT="${ADMIN_PORT:-$((API_PORT + 1))}"
# 注意:健康检查与汇总输出用的地址不在这里写死——真实监听地址来自
# deploy/.env 的 HOST,脚本在源入 .env 后计算 BIND_HOST / HEALTH_HOST / LAN_IP。
@@ -114,6 +116,9 @@ set -a
. ./deploy/.env
set +a
API_PORT="${PORT:-3000}"
# 管理端端口:默认 API 端口 + 1;等于 API 端口时是单端口兼容模式
ADMIN_PORT="${ADMIN_PORT:-$((API_PORT + 1))}"
ADMIN_BIND_HOST="${ADMIN_HOST:-${HOST:-0.0.0.0}}"
[ -n "${DB_HOST:-}" ] || die "deploy/.env 中 DB_HOST 未设置"
[ -n "${DB_USER:-}" ] || die "deploy/.env 中 DB_USER 未设置"
@@ -398,6 +403,21 @@ if [ "$READY" -ne 1 ]; then
fi
log "API 已就绪:$(http_get "http://${HEALTH_HOST}:${API_PORT}/health" | head -c 200)"
# 管理端端口单独确认(端口写错时,只有在装的时候点出来才有人看见)
ADMIN_READY=""
ADMIN_HOST_FOR_PROBE="${ADMIN_BIND_HOST}"
case "$ADMIN_HOST_FOR_PROBE" in
0.0.0.0|'*'|'') ADMIN_HOST_FOR_PROBE="127.0.0.1" ;;
esac
if [ "$ADMIN_PORT" = "$API_PORT" ]; then
ADMIN_READY="same"
elif http_ok "http://${ADMIN_HOST_FOR_PROBE}:${ADMIN_PORT}/health"; then
ADMIN_READY="yes"
else
ADMIN_READY="no"
warn "管理端端口 ${ADMIN_PORT} 未在本机就绪(API 端口=${API_PORT})"
fi
# ---------- 5. 边界:确认"外部到底能不能访问" ----------
# 绑定层写错(HOST=127.0.0.1)时,本机 curl 一切正常,但外部永远连不上,
# 而安装日志里看不出任何异常——必须在安装时就点出来。
@@ -425,10 +445,14 @@ cat <<EOF
------------------------------------------------------------
反向代理说明(本脚本不部署反代)
------------------------------------------------------------
本服务已同时提供 API 与管理端页面,反代只需将请求整体转发至:
http://<本机地址>:${API_PORT}
管理端口与软件 API 端口是分开的,按用途分别转发:
健康检查(可用于反代的存活探测):http://<本机地址>:${API_PORT}/health
软件 API 端口(给 Windows/Godot 客户端与 Android App):http://<本机地址>:${API_PORT}
提供 /api、/health、/static、/contracts;**不含管理页面**(未知路径返回 404)
管理端端口(给浏览器):http://<本机地址>:${ADMIN_PORT}
提供管理页面 + 上面全部接口;建议只在内网 / VPN 内暴露
健康检查(可用于反代的存活探测,两个端口都提供):/health
上传上限需在代理层同步放开(本服务为 $(( ${UPLOAD_MAX_BYTES:-536870912} / 1024 / 1024 )) MiB)
配置样例与完整说明:
@@ -437,7 +461,7 @@ cat <<EOF
deploy/proxy/README.md IIS / nginx 反代说明文档
注意:若反代与后端不在同一台机器,需保持 HOST=0.0.0.0(当前值:${BIND_HOST})并用防火墙限制来源;
若在同一台机器,建议把 HOST 改为 127.0.0.1。
若在同一台机器,建议把 HOST 改为 127.0.0.1(管理端可用 ADMIN_HOST 单独限制,当前值:${ADMIN_BIND_HOST})。
------------------------------------------------------------
EOF
@@ -497,16 +521,17 @@ cat <<EOF
============================================================
OpenDance Studio Web 服务端已部署(业务服务,无 Docker、不含反向代理)
监听地址: ${LISTEN_DESC}
管理端首页: http://${HOST_HINT}:${API_PORT}/ (由本服务直接提供)
API 前缀: http://${HOST_HINT}:${API_PORT}/api
软件 API: ${LISTEN_DESC}
接口:/api、/health、/static、/contracts(客户端调用;不含管理页面)
管理端: http://${HOST_HINT}:${ADMIN_PORT}/(浏览器访问管理界面)
健康检查: http://${HOST_HINT}:${API_PORT}/health
契约快照: http://${HOST_HINT}:${API_PORT}/contracts/
数据库: 使用现有实例 ${DB_HOST}:${DB_PORT:-3306}/${DB_NAME}
上传目录: ${STORAGE_DIR}
程序目录: ${APP_DIR}
进程管理: ${INIT_SYSTEM}
HOST 配置: ${BIND_HOST}(部署配置文件 ${APP_DIR}/deploy/.env)
端口配置: API ${API_PORT} / 管理端 ${ADMIN_PORT}(${APP_DIR}/deploy/.env)
绑定配置: HOST=${BIND_HOST} ADMIN_HOST=${ADMIN_BIND_HOST}
常用命令:
${CMD_HINT}
@@ -534,9 +559,9 @@ EOF
esac
cat <<EOF
别忘了在防火墙放行 ${API_PORT}/tcp:
firewall-cmd --permanent --add-port=${API_PORT}/tcp && firewall-cmd --reload
# 或:ufw allow ${API_PORT}/tcp
别忘了在防火墙放行端口(客户端用 ${API_PORT},浏览器管理界面用 ${ADMIN_PORT}):
firewall-cmd --permanent --add-port=${API_PORT}/tcp --add-port=${ADMIN_PORT}/tcp && firewall-cmd --reload
# 或:ufw allow ${API_PORT}/tcp && ufw allow ${ADMIN_PORT}/tcp
诊断脚本(只读):bash ${APP_DIR}/deploy/check-access.sh
============================================================
@@ -549,8 +574,8 @@ elif [ "$EXTERNAL_OK" = "no" ]; then
============================================================
绑定层没问题,外部却连不上,通常是防火墙或云安全组拦截:
1) 放行端口:firewall-cmd --permanent --add-port=${API_PORT}/tcp && firewall-cmd --reload
或 ufw allow ${API_PORT}/tcp
1) 放行端口:firewall-cmd --permanent --add-port=${API_PORT}/tcp --add-port=${ADMIN_PORT}/tcp && firewall-cmd --reload
或 ufw allow ${API_PORT}/tcp && ufw allow ${ADMIN_PORT}/tcp
2) 云服务器还要在控制台「安全组 / 网络 ACL」放行 ${API_PORT}/tcp
3) 确认客户端与服务器网络互通