diff --git a/DSH.md b/DSH.md index 24fb2ef..3539dcc 100644 --- a/DSH.md +++ b/DSH.md @@ -30,7 +30,7 @@ opendance-web ←── 本仓库 | 数据库 | **现有 MySQL 兼容实例**(当前部署为 MariaDB 10.11.19),mysql2 驱动 | | 管理前端 | React 18 + Vite + TypeScript | | 3D 预览 | three.js + @pixiv/three-vrm | -| 多格式解析 | BVH 内置纯 TS 解析器;FBX/glTF 走可选外部 Assimp | +| 多格式解析 | BVH / VMD 内置纯 TS 解析器;FBX/glTF 走可选外部 Assimp | | 认证 | **本版本不含认证**(内网部署);上线前须补 ≥ T-WEB-011 | ## 3. 单次会话加载范围 @@ -77,9 +77,15 @@ opendance-web/ │ └── open-source-credits.md # 开源声明锚点(三端共用) ├── web-server/ # NestJS 服务端 │ ├── src/ # 源码(entities / modules / preprocess) +│ │ └── preprocess/ # BVH / VMD / FBX → MediaPipe33 +│ │ ├── vmd-parser.ts # VMD 二进制 + Shift-JIS + 贝塞尔插值 +│ │ ├── mmd-skeleton.ts # 标准 MMD 骨架(静止比例实测自参考 PMX) +│ │ ├── mmd-pose.ts # MMD FK + 两骨骼腿部 IK + 左手系→右手系 Z 镜像 +│ │ └── retarget.ts # worldFramesToKeypointFrames():BVH / VMD 共用 │ ├── admin/ # React 管理端 -│ ├── test/ # 单元测试 + 端到端冒烟 +│ ├── test/ # 单元测试 + 端到端冒烟(含 mmd-vmd.spec.ts) │ └── scripts/ # migrate.mjs / check-contracts.mjs / check-routes.mjs +│ # / inspect-vmd.mjs(VMD 诊断工具) ├── deploy/ # ★ 业务服务部署(不含 MySQL、不含反代) │ ├── install.sh # 一键部署(systemd / OpenRC / SysVinit / 后台进程) │ ├── uninstall.sh @@ -99,6 +105,8 @@ opendance-web/ **也不使用 Docker**,**也不部署反向代理**:本端只提供业务服务 (API + 自带的管理端页面),反代由另外的服务负责, 本项目只在 `deploy/proxy/` 下提供 IIS 与 nginx 的配置说明文档。 + 业务服务监听**两个端口**(软件 API 端口 `PORT` + 管理端端口 `ADMIN_PORT`), + 约定见 §9。 2. **`DB_SYNCHRONIZE` 生产必须为 `false`**,所有表结构变更走 migration;每个 migration 必须可逆。 3. **绝不把凭证写入仓库**。`.env` 已在 `.gitignore` 中,只提交 `.env.example`。 @@ -143,13 +151,55 @@ git push origin task/T-WEB-0XX ``` DB_HOST DB_PORT DB_USER DB_PASS DB_NAME DB_SYNCHRONIZE=false +PORT=3000 # 软件 API 端口(客户端调用,不含管理页面) +ADMIN_PORT=3001 # 管理端端口(默认 PORT + 1;等于 PORT 为单端口兼容模式) +HOST=0.0.0.0 # 软件 API 监听地址 +ADMIN_HOST=0.0.0.0 # 可选,管理端监听地址,默认跟随 HOST STORAGE_DIR=/data/storage UPLOAD_MAX_BYTES=536870912 ASSIMP_BIN=assimp # 可选 CONTRACTS_DIR=/contracts # 可选,读取实时契约版本 ``` -## 9. 上下文超标时的拆分策略 +## 9. 双端口约定(软件 API 端口 / 管理端端口) + +- **软件 API 端口**:`PORT`,默认 **3000**。给 Windows/Godot 客户端与 Android App + 调用,只提供 `/api`、`/health`、`/static`、`/contracts`;**不提供管理页面**, + `/`、`/songs` 等一律 404(有意设计,避免管理页暴露在客户端调用的端口上)。 +- **管理端端口**:`ADMIN_PORT`,默认 `PORT + 1`(即 **3001**)。给浏览器访问, + 提供管理页面(含 SPA 深链接回退)与上面全部接口。 +- `ADMIN_HOST` 可选,默认跟随 `HOST`;可用它把管理端限制在本机或内网。 +- `ADMIN_PORT === PORT` 为**单端口兼容模式**:管理页面回到 API 端口,日志提示 + 「未分端口」,只 warn 不中止启动。 +- 端口非法(非 1..65535 整数)在生产环境会中断启动。 +- 启动日志分别打印两行:`软件 API 端口:…` 与 `管理端端口:…`。 +- 反向代理按用途分别转发两个端口;`/health` 两个端口都提供,可用于存活探测。 + 细则见 `deploy/proxy/README.md` 与 `tasks/010-deploy.md`。 + +## 10. VMD 动作通路(MikuMikuDance) + +``` +.vmd ─▶ vmd-parser.ts ─▶ mmd-skeleton.ts ─▶ mmd-pose.ts ─▶ retarget.ts + 二进制 + Shift-JIS 标准 MMD 骨架 FK + 腿部 IK worldFramesToKeypointFrames() + + 贝塞尔插值 + Z 镜像 (与 BVH 共用) +``` + +- `MotionFormat.Vmd = 'vmd'`,扩展名 `.vmd`,固定 **30 fps**(上传时填写的 `fps` 被忽略)。 +- VMD 二进制:30 字节头 + 20 字节 Shift-JIS 模型名 + 111 字节/条的骨骼关键帧 + morph 段; + 只有旋转(少数骨骼带位移)、不含骨骼长度,因此必须靠 `mmd-skeleton.ts` 的标准骨架 + (静止比例从参考 PMX 实测)做 FK。 +- **腿部驱动自动判定**:`足IK` 有非零位移 → 两骨骼解析 IK(膝盖朝角色正面弯, + 不可达时收敛为伸直);否则用 FK 旋转。部分动作数据把腿烘焙成 FK。 +- **手性**:MMD 为左手系,统一做 Z 镜像(旋转 `(x,y,z,w) → (-x,-y,z,w)`、位移 z 取反) + 后进入右手系通路。由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` + 下「膝盖朝前」占 94.1%,不转换仅 5.9%。 +- `worldFramesToKeypointFrames()` 为 BVH / VMD 共用:骨盆归零 / 自适应缩放 / + 图像空间投影 / 9 个关节角完全一致。 +- 诊断工具:`node scripts/inspect-vmd.mjs 动作.vmd [--frames N] [--json out.json]` + (打印头部/模型名/帧数/轨道数、未识别骨骼、IK 还是 FK、关键点序列异常统计、膝肘角分布)。 +- 契约 `contracts/model-formats.md` §2.3;测试 `test/mmd-vmd.spec.ts`(31 项)。 + +## 11. 上下文超标时的拆分策略 | 超标原因 | 拆分方式 | |---|---| diff --git a/README.txt b/README.txt index 80f8f52..6f4a52a 100644 --- a/README.txt +++ b/README.txt @@ -18,13 +18,19 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 只在 deploy/proxy/ 下提供 IIS 与 nginx 的配置说明文档。 部署形态 - Node 业务服务由系统服务托管,同时提供: - / 管理端页面(本服务自带,含 SPA 深链接回退) + Node 业务服务由系统服务托管,同时监听**两个端口**,职责分开: + + 软件 API 端口(默认 3000,给 Windows/Godot 客户端与 Android App) /api/ REST API /health 健康检查 /static/ 上传文件 /contracts/ 契约快照 - 因为静态页面也由本服务提供,反向代理**只需一条全匹配转发规则**。 + 该端口**不提供管理页面**,/ 与 /songs 等一律返回 404(有意设计) + + 管理端端口(默认 3001,给浏览器) + 管理页面(本服务自带,含 SPA 深链接回退)+ 上面全部接口 + + 反向代理按用途分别转发两个端口,每个端口各一条全匹配转发规则即可。 前置要求 - Linux 服务器,有 root 权限(不需要 sudo,直接以 root 执行即可) @@ -56,18 +62,23 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 注册并启动系统服务(systemd / OpenRC / SysVinit / 后台进程)→ 等待健康检查 部署完成后(直接访问后端端口, 为服务器 IP) - 管理端首页: http://:3000/ - API 前缀: http://:3000/api + 客户端接口: http://:3000/api 健康检查: http://:3000/health 契约快照: http://:3000/contracts/ + 管理端首页: http://:3001/ + (软件 API 端口上没有管理页面:http://:3000/ 返回 404,属正常) 对外访问 - 由反向代理把请求转发到 http://<后端地址>:3000 即可。 - 配置样例与说明: + 由反向代理**按用途分别转发两个端口**: + 浏览器管理界面 → http://<后端地址>:3001 + Windows/Godot 客户端与 Android App → http://<后端地址>:3000 + 配置样例与说明(含两个端口的转发写法): deploy/proxy/nginx.conf.example nginx 站点配置 deploy/proxy/web.config.example IIS(ARR + URL Rewrite)配置 deploy/proxy/README.md 完整说明、自检清单与常见故障 + 直连后端时,防火墙需同时放行两个端口(默认 3000 与 3001)。 + 外部访问不了?安装脚本结尾会直接给出结论(监听地址、对外可达性、 防火墙提示);也可以在服务器上跑一次自检脚本(只读,不改配置): bash deploy/check-access.sh # 或 bash deploy/check-access.sh /opt/opendance @@ -78,6 +89,8 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 APP_DIR=/opt/opendance 程序目录 SERVICE_USER=opendance 运行服务的系统用户 STORAGE_DIR=/var/lib/opendance/storage 上传文件目录 + PORT=3000 软件 API 端口 + ADMIN_PORT=3001 管理端端口(默认 PORT + 1) 例:APP_DIR=/srv/opendance bash deploy/install.sh @@ -118,7 +131,7 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 cp .env.example .env # 填写 DB_* npm install npm run migration:run # 首次需要 - npm run start:dev # http://localhost:3000 + npm run start:dev # API http://localhost:3000;管理端 http://localhost:3001 管理前端 cd web-server/admin @@ -132,7 +145,7 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 cd web-server npm run contracts:check # 契约版本一致性 npm run typecheck # TypeScript 严格检查 - npm run test # 31 项单元测试 + npm run test # 85 项单元测试(含 31 项 VMD) npm run build # 生产构建 npm run start:prod & # 启动 npm run test:smoke # 57 项端到端冒烟(需要数据库可连通) @@ -151,9 +164,18 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 所有表结构变更通过 TypeORM migration 执行。 DB_LOGGING 是否打印 SQL(默认 false) - PORT API 监听端口(默认 3000) - HOST 监听地址。反代与后端同机用 127.0.0.1(推荐); - 反代在别的机器用 0.0.0.0,并用防火墙限制来源 + PORT 软件 API 监听端口(默认 3000)。给 Windows/Godot 客户端 + 与 Android App 调用,只提供 /api、/health、/static、 + /contracts,**不提供管理页面** + ADMIN_PORT 管理端端口(默认 PORT + 1,即 3001)。浏览器访问管理界面 + (管理页面 + 上面全部接口);设为与 PORT 相同即为 + 单端口兼容模式,日志会提示「未分端口」 + HOST 监听地址(软件 API 端口)。反代与后端同机用 127.0.0.1 + (推荐);反代在别的机器用 0.0.0.0,并用防火墙限制来源 + ADMIN_HOST 管理端监听地址(可选,默认跟随 HOST)。需要接口对外、 + 管理端只对本机时设为 127.0.0.1 + 端口配置非法(非 1..65535 整数)时,生产环境启动会直接中止; + ADMIN_PORT 设为与 PORT 相同只打印提示,不中止。 ADMIN_DIR 管理端静态站点目录(默认 ../admin,即 /opt/opendance/admin) STORAGE_DIR 上传文件存储目录(原生部署默认 /var/lib/opendance/storage) UPLOAD_MAX_BYTES 单文件上限,默认 536870912(512 MiB)。 @@ -177,7 +199,7 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 POST /api/songs/:id/audio 上传音频 GET /api/motions 动作列表 POST /api/motions 上传动作 + 来源信息 - POST /api/motions/:id/preprocess BVH/FBX → MediaPipe33 关键点 + POST /api/motions/:id/preprocess BVH/VMD/FBX → MediaPipe33 关键点 GET /api/motions/:id/keypoints 下载关键点序列(NDJSON) GET /api/models 模型列表 POST /api/models 上传模型 + 来源信息(VRM 优先) @@ -193,6 +215,28 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 上传文件通过 /static////.ext 访问, 相同内容只落盘一次(sha256 去重)。 +支持的动作格式(POST /api/motions 上传 → POST /api/motions/:id/preprocess 预处理) + BVH .bvh 内置纯 TS 解析器,保留源帧率 + VMD .vmd 内置解析(MikuMikuDance),固定 30fps; + 腿部按「足IK是否带位移」自动选 IK 或 FK + FBX .fbx 需可选的 Assimp 先转 BVH + glTF / GLB .gltf / .glb 同上 + 关键点 JSON .json 已是 MediaPipe33 结构,只做校验与规范化 + +VMD 动作上传前后自查 + 服务端对 .vmd 是内置解析,不依赖 Assimp。上传前可先在 web-server 目录下 + 跑诊断工具(只读:不写数据库、不访问网络),确认骨骼名能识别、腿走 IK + 还是 FK、关键点序列无异常: + + cd web-server + npm run build + node scripts/inspect-vmd.mjs 动作.vmd + node scripts/inspect-vmd.mjs 动作.vmd --frames 300 # 只采样前 300 帧 + node scripts/inspect-vmd.mjs 动作.vmd --json out.json # 导出关键点序列 + + 它会打印头部 / 模型名 / 帧数 / 轨道数、未识别骨骼、走 IK 还是 FK、关键点 + 序列异常统计与膝肘角分布。上传后再调用 /preprocess 走正式通路。 + ---------------------------------------------------------------- 六、目录结构 ---------------------------------------------------------------- diff --git a/contracts/VERSION b/contracts/VERSION index 66b5603..4044fac 100644 --- a/contracts/VERSION +++ b/contracts/VERSION @@ -1,8 +1,8 @@ # OpenDance Studio 契约版本 # 规则:任何字段级变更必须提升对应条目版本,并回到 opendance-web 修改后再向另两端建迁移任务。 -rest-api: 0.1.0 +rest-api: 0.2.0 websocket-keypoints: 0.1.0 -data-models: 0.1.0 +data-models: 0.2.0 skeleton-topology: 0.1.0 -model-formats: 0.1.0 +model-formats: 0.2.0 database: 0.1.0 diff --git a/contracts/data-models.md b/contracts/data-models.md index 9acd409..5037968 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -1,4 +1,4 @@ -# contracts/data-models.md — 数据模型契约 v0.1.0 +# contracts/data-models.md — 数据模型契约 v0.2.0 Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视图**。 字段名在 JSON 中统一 `camelCase`;数据库列名 `snake_case`(见 `database.md`)。 @@ -75,7 +75,7 @@ rejected 已驳回 | `id` | number | 读 | | | `songId` | number\|null | | 关联曲目 | | `name` | string | ✅ | 动作名称,≤200 | -| `format` | enum | ✅ | `bvh` / `fbx` / `gltf` / `glb` / `keypoints-json` | +| `format` | enum | ✅ | `bvh` / `vmd` / `fbx` / `gltf` / `glb` / `keypoints-json` | | `fileUrl` | string | 读 | 原始动作文件 | | `fileSize` | number | 读 | 字节 | | `fps` | number\|null | | 原始帧率 | diff --git a/contracts/model-formats.md b/contracts/model-formats.md index c4af693..b31f67a 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -1,4 +1,4 @@ -# contracts/model-formats.md — 模型与动作格式契约 v0.1.0 +# contracts/model-formats.md — 模型与动作格式契约 v0.2.0 ## 1. 角色模型(models) @@ -19,6 +19,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) | 格式 | 扩展名 | 预处理支持 | 说明 | |---|---|---|---| | BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 → FK → MediaPipe33 | +| VMD | `.vmd` | ✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 → MediaPipe33,见 §2.3 | | FBX | `.fbx` | ⚠️ 需 Assimp | 服务端调用 `assimp` 可执行文件导出为 BVH/glTF 后再解析;未安装则 `preprocessState=failed` | | glTF/GLB | `.gltf` / `.glb` | ⚠️ 需 Assimp | 同上 | | 关键点 JSON | `.json` | ✅ 直通校验 | 已是 `mediapipe33@0.1.0` 结构,仅做校验与归一化 | @@ -65,6 +66,47 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) 未映射到的点按 `skeleton-topology.md` §5 的层级由父节点推算, 并置 `confidence` 降低(`0.5`),表示估算而非实测。 +### 2.3 VMD(MikuMikuDance)解析约定 + +VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节, +Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**:15 字节骨骼名、uint32 帧号、 +float32×3 位移、float32×4 旋转四元数、64 字节贝塞尔插值参数)+ morph / 相机 / +灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移), +因此服务端必须自带一套标准 MMD 骨架才能做 FK。 + +处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)→ `mmd-skeleton.ts` +(标准骨架)→ `mmd-pose.ts`(FK + 腿部 IK)→ `retarget.ts` 的 +`worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 / +9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。 + +| MediaPipe 点 | MMD 标准骨骼 | 说明 | +|---|---|---| +| 0 nose | `頭` | 取头骨根部再上移,近似面部中心 | +| 11 / 12 left/right_shoulder | `左腕` / `右腕` | 肩关节(不是 `左肩` 骨骼) | +| 13 / 14 left/right_elbow | `左ひじ` / `右ひじ` | | +| 15 / 16 left/right_wrist | `左手首` / `右手首` | | +| 23 / 24 left/right_hip | `左足` / `右足` | 大腿根部 | +| 25 / 26 left/right_knee | `左ひざ` / `右ひざ` | | +| 27 / 28 left/right_ankle | `左足首` / `右足首` | | +| 31 / 32 left/right_foot_index | `左つま先` / `右つま先` | | + +骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法); +未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。 + +**腿部驱动判定**:`足IK` 轨道存在非零位移 → 走 IK(用两骨骼解析 IK 把踝拉到 IK +目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成 +FK 旋转)。两份示例文件分别命中两种路径。 + +**坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色 +左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) → (-x,-y,z,w)`、位移 z 取反)后 +送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下 +膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。 + +**标准骨架数值来源**:解析参考模型的 PMX 骨骼段实测得到(而非凭经验估计), +再去掉模型自身原点偏移并抹平左右不对称。关键比例:足首 1.43 / ひざ 6.57 / +髋 11.50 / センター 10.88 / 首 16.21 / 頭 17.16(总高约 19.4,髋高比约 59%); +大腿 4.94 + 小腿 5.15 ≈ 腿长 10.07。 + ## 3. 上传限制 | 项 | 值 | diff --git a/contracts/rest-api.yaml b/contracts/rest-api.yaml index 7c2e9f7..ed89909 100644 --- a/contracts/rest-api.yaml +++ b/contracts/rest-api.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: OpenDance Studio Web 管理服务端 API - version: 0.1.0 + version: 0.2.0 description: | OpenDance Studio 的唯一数据源与契约权威。 契约版本见 contracts/VERSION。字段语义见 contracts/data-models.md。 @@ -11,11 +11,14 @@ info: - 来源五元组(sourceType/sourceName/sourceUrl/license/author)在三类资产上均为必填 servers: - url: http://localhost:3000 - description: 本地开发 - - url: http:// + description: 本地开发(软件 API 端口) + - url: http://:3000 description: >- - Linux 业务服务部署。服务自带管理端页面,反向代理(外部提供,非本项目交付物) - 只需将请求整体转发到该地址即可,无需托管静态文件或配置 SPA 回退。 + Linux 业务服务部署的**软件 API 端口**(`PORT`,默认 3000)。Windows/Godot 客户端与 + Android App 调用本端口的 /api、/health、/static、/contracts。 + 管理端页面在**管理端口**(`ADMIN_PORT`,默认 3001),该端口同时提供全部接口; + API 端口不提供管理页面,未命中的路径一律 404。 + 反向代理由另外的服务提供(非本项目交付物),按用途分别转发到对应端口即可。 tags: - name: health @@ -180,7 +183,7 @@ paths: schema: { type: integer, format: int64 } - name: format in: query - schema: { type: string, enum: [bvh, fbx, gltf, glb, keypoints-json] } + schema: { type: string, enum: [bvh, vmd, fbx, gltf, glb, keypoints-json] } - name: status in: query schema: { $ref: '#/components/schemas/PublishStatus' } @@ -589,7 +592,7 @@ components: enum: [draft, pending, published, rejected] MotionFormat: type: string - enum: [bvh, fbx, gltf, glb, keypoints-json] + enum: [bvh, vmd, fbx, gltf, glb, keypoints-json] ModelFormat: type: string enum: [vrm, glb, gltf, fbx] diff --git a/deploy/.env.example b/deploy/.env.example index aa0db3a..1e39d8b 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -7,13 +7,20 @@ # ---- 服务 ---- NODE_ENV=production +# 软件 API 端口:Windows/Godot 客户端与 Android App 调用它 +# (提供 /api、/health、/static、/contracts,**不提供管理页面**) PORT=3000 +# 管理端端口:浏览器访问管理界面(页面 + 上面全部接口) +# 不设置时默认 = PORT + 1(即 3001);设为与 PORT 相同即为单端口兼容模式 +ADMIN_PORT=3001 # 监听地址: # 反代与后端同机 → 127.0.0.1(后端不直接对外暴露,推荐) # 反代在别的机器 → 0.0.0.0,并用防火墙把该端口限制为仅反代来源可访问 HOST=0.0.0.0 +# 管理端单独绑定地址(可选)。只想让管理端暴露在内网时,例如 127.0.0.1 或内网网卡地址 +# ADMIN_HOST=127.0.0.1 -# 管理端静态站点目录(本服务自带管理端页面,反代只需整体转发到本端口) +# 管理端静态站点目录(本服务自带管理端页面,反代只需整体转发到管理端端口) ADMIN_DIR=/opt/opendance/admin # ---- 现有 MySQL / MariaDB 实例(本项目不安装、不管理数据库)---- diff --git a/deploy/install.sh b/deploy/install.sh index 28907c2..fc7b567 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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 <:${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 < > 安装脚本 `deploy/install.sh` 不会安装或修改任何反代软件。 -## 一、需要转发的目标 +## 一、需要转发的目标(两个端口,按用途分开) -本服务**自带管理端页面**,因此反向代理**只需把请求整体转发**到一个地址: +本服务**同时监听两个端口**,职责不同: + +| 端口 | 环境变量 | 默认 | 给谁用 | 提供什么 | +|---|---|---|---|---| +| 软件 API 端口 | `PORT` | `3000` | Windows/Godot 客户端、Android App | `/api`、`/health`、`/static`、`/contracts`;**不含管理页面**(未知路径 404) | +| 管理端端口 | `ADMIN_PORT` | `3001` | 浏览器(管理员) | 管理页面 + 上面全部接口 | ``` -http://<后端地址>:3000 +软件 API: http://<后端地址>:3000 +管理端: http://<后端地址>:3001 ``` -不需要在代理层单独托管静态文件,也不需要配置 SPA 回退(服务端已处理)。 -一个 `location /`(nginx)或一条全匹配规则(IIS)即可。 +两者都不需要在代理层托管静态文件,也不需要配置 SPA 回退(服务端已处理); +各用一条 `location /`(nginx)或全匹配规则(IIS)即可。 -| 路径 | 内容 | 说明 | +**推荐做法**:把两个端口挂在两个不同的站点/域名上,便于分别做访问控制,例如 + +- `https://dance-api.example.com` → 转发到 `3000`(给客户端,可用 IP 白名单) +- `https://dance-admin.example.com` → 转发到 `3001`(给管理员,建议加 Basic Auth / SSO 或只在内网放行) + +如果客户端与管理端在同一个内网、也不想配域名,可以**让客户端直连 `3000`**(不经过反代), +只把管理端 `3001` 挂到反代上;反之亦可。 + +| 路径 | 内容 | 哪个端口提供 | |---|---|---| -| `/` | 管理端页面 | 由 Node 服务提供;前端路由深链接(如 `/songs`)由服务端回退到 `index.html` | -| `/assets/*` | 前端静态资源 | 带内容哈希,可长缓存 | -| `/api/*` | REST API | 契约见 `contracts/rest-api.yaml` | -| `/health` | 健康检查 | 可用作反代的存活探测,数据库不可用时返回 **503** | -| `/static/*` | 上传文件 | 模型 / 音频 / 关键点序列 | -| `/contracts/*` | 契约快照 | 只读,便于另两端查阅 | +| `/`、`/assets/*` | 管理端页面与静态资源 | **仅管理端**;前端路由深链接(如 `/songs`)由服务端回退到 `index.html` | +| `/api/*` | REST API | 两个端口都提供(契约见 `contracts/rest-api.yaml`) | +| `/health` | 健康检查 | 两个端口都提供;可用作反代的存活探测,数据库不可用时返回 **503** | +| `/static/*` | 上传文件 | 两个端口都提供(模型 / 音频 / 关键点序列) | +| `/contracts/*` | 契约快照 | 两个端口都提供,只读 | > Windows 客户端与 Android 之间的**关键点 WebSocket 链路不经过本服务**, > 反代无需配置 WebSocket 转发。 +> +> `ADMIN_PORT` 设为与 `PORT` 相同时是**单端口兼容模式**:两个端口合一, +> 管理页面重新出现在 API 端口上(此时反代只需要一条规则)。 ## 二、后端绑定地址怎么选 -`deploy/.env` 中的 `HOST`: +`deploy/.env` 中的 `HOST`(软件 API 端口)与可选的 `ADMIN_HOST`(管理端端口,缺省跟随 `HOST`): | 部署形态 | 建议值 | 说明 | |---|---|---| -| 反代与后端**同机** | `127.0.0.1` | 后端不直接对外暴露,最安全 | -| 反代与后端**不同机** | `0.0.0.0` | 必须用防火墙把 3000 端口限制为仅反代来源可访问 | +| 反代与后端**同机** | `HOST=127.0.0.1` | 后端不直接对外暴露,最安全 | +| 反代与后端**不同机** | `HOST=0.0.0.0` | 必须用防火墙把端口限制为仅反代来源可访问 | +| 只想让管理端在内网可见 | `ADMIN_HOST=<内网网卡地址>` | 与 `HOST=0.0.0.0` 组合使用,把管理端限制在内网 | 改完执行 `systemctl restart opendance-api`(或对应的 `ctl.sh restart`)生效。 @@ -154,8 +171,12 @@ IIS 管理器 → 选中服务器节点 → Application Request Routing Cache 配置完成后逐项确认: ```bash +# 0. 两个端口都在监听(双端口部署) +ss -tlnp | grep -E ':(3000|3001)' + # 1. 后端自身可用(绕过反代) curl -i http://127.0.0.1:3000/health +curl -i http://127.0.0.1:3001/health # 2. 经反代可访问 curl -i http://<反代地址>/health @@ -180,6 +201,9 @@ curl -i http://<反代地址>/api/catalog/version | 上传大文件 413 | 代理层上传上限没放开(见第三、四、五节) | | 上传中途断开 | 代理超时太短 | | `/songs` 直接访问 404 | SPA 回退被代理拦截;本服务已处理,代理**不要**自己加 `try_files`,全匹配转发即可 | +| 客户端接口全 404 | 把客户端的转发规则指到了管理端口之外,或误把 `/api` 前缀重写掉;客户端应转发到软件 API 端口 | +| 管理页面 404 | 把浏览器的转发规则指到了**软件 API 端口**(该端口不提供管理页面,这是有意设计);改指 `ADMIN_PORT` | +| 两个端口都转发到同一条规则 | 若确实想合并两个端口,设置 `ADMIN_PORT=PORT` 走单端口模式,而不是让反代去猜 | ## 八、还没有反代时:直连后端怎么访问 diff --git a/deploy/proxy/nginx.conf.example b/deploy/proxy/nginx.conf.example index 1d14419..eca3186 100644 --- a/deploy/proxy/nginx.conf.example +++ b/deploy/proxy/nginx.conf.example @@ -1,44 +1,57 @@ # ============================================================ -# OpenDance Studio Web 服务端 —— nginx 反向代理样例 +# OpenDance Studio Web 服务端 —— nginx 反向代理样例(双端口) # # 说明:本 Web 端不自带反向代理。本文件是**给你的反代服务参考**的配置样例, # 由部署 `deploy/install.sh` 之外的反代负责人使用。 # -# 特点:后端(NestJS)自带管理端页面与 SPA 回退,因此这里 -# 只需一个全匹配 location 把请求整体转发过去即可, -# 不需要 root/try_files 托管静态文件。 +# 后端同时监听两个端口,职责不同: +# 软件 API 端口(PORT,默认 3000)—— Windows/Godot 客户端、Android App +# 只提供 /api、/health、/static、/contracts +# 管理端端口(ADMIN_PORT,默认 3001)—— 浏览器,提供管理页面 + 上面全部接口 +# +# 两个端口都不需要在代理层托管静态文件、也不需要 SPA 回退(服务端已处理), +# 因此每个站点只要一条 `location /` 全匹配转发即可。 # # 安装: # Debian/Ubuntu: /etc/nginx/sites-available/opendance.conf(再软链到 sites-enabled/) # RHEL 系: /etc/nginx/conf.d/opendance.conf # 校验并重载: nginx -t && systemctl reload nginx +# +# 想省事只挂一个域名?把两段 server 合并成一段,让另一侧的客户端直连端口即可; +# 也可以在后端设 `ADMIN_PORT=PORT` 走单端口兼容模式(此时管理页面回到 API 端口)。 # ============================================================ upstream opendance_api { - # 后端地址:同机用 127.0.0.1;异机改成实际 IP,并在后端放防火墙 + # 软件 API 端口。同机用 127.0.0.1;异机改成实际 IP,并在后端放防火墙 server 127.0.0.1:3000; keepalive 16; } +upstream opendance_admin { + # 管理端端口 + server 127.0.0.1:3001; + keepalive 16; +} + +# ------------------------------------------------------------ +# 站点一:软件 API(给客户端调用) +# ------------------------------------------------------------ server { listen 80; listen [::]:80; - server_name your.domain.com; # 改成实际域名或 _ + server_name dance-api.example.com; # 改成实际域名或 _ - # 字符集:管理端与 API 都是 UTF-8 charset utf-8; # ★ 上传上限:后端默认允许 512 MiB,nginx 默认仅 1 MB, - # 不改会导致 VRM / FBX 等大文件上传直接 413。 + # 不改会导致 VRM / FBX / VMD 等大文件上传直接 413。 client_max_body_size 512m; client_body_timeout 300s; - access_log /var/log/nginx/opendance.access.log; - error_log /var/log/nginx/opendance.error.log; + access_log /var/log/nginx/opendance-api.access.log; + error_log /var/log/nginx/opendance-api.error.log; - # ---- 全部请求转发给后端 ---- - # 管理端页面、前端路由深链接、/api、/health、/static、/contracts - # 全部由后端处理,所以这里只需一条规则。 + # ---- 全部请求转发给后端(/api、/health、/static、/contracts)---- location / { proxy_pass http://opendance_api; proxy_http_version 1.1; @@ -72,14 +85,54 @@ server { gzip_types text/plain text/css application/json application/javascript application/x-ndjson text/xml application/xml image/svg+xml; gzip_min_length 1024; +} - # 若前端资源由后端返回,代理层不要缓存 HTML,避免升级后页面不更新 - location ~* \.html$ { - proxy_pass http://opendance_api; +# ------------------------------------------------------------ +# 站点二:管理端(给浏览器) +# +# 建议只在内网 / VPN 内暴露,或至少加一层认证(见文件末尾)。 +# 也可以把 server_name 写成内网域名,并在防火墙只放行内网来源。 +# ------------------------------------------------------------ +server { + listen 80; + listen [::]:80; + server_name dance-admin.example.com; # 改成实际域名或 _ + + charset utf-8; + + # 管理端同样会上传模型 / 动作,上限保持一致 + client_max_body_size 512m; + client_body_timeout 300s; + + access_log /var/log/nginx/opendance-admin.access.log; + error_log /var/log/nginx/opendance-admin.error.log; + + # ---- 管理页面 + 全部接口 ---- + location / { + proxy_pass http://opendance_admin; proxy_http_version 1.1; - proxy_set_header Host $host; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + proxy_connect_timeout 30s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_request_buffering off; + proxy_buffering off; + + # 代理层不要缓存 HTML,避免升级后页面不更新 add_header Cache-Control "no-cache"; } + + gzip on; + gzip_types text/plain text/css application/json application/javascript + application/x-ndjson image/svg+xml; + gzip_min_length 1024; } # ============================================================ @@ -88,7 +141,7 @@ server { # server { # listen 443 ssl; # http2 on; -# server_name your.domain.com; +# server_name dance-admin.example.com; # # ssl_certificate /etc/nginx/ssl/opendance.crt; # ssl_certificate_key /etc/nginx/ssl/opendance.key; @@ -97,7 +150,7 @@ server { # client_max_body_size 512m; # # location / { -# proxy_pass http://opendance_api; +# proxy_pass http://opendance_admin; # proxy_http_version 1.1; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; @@ -111,11 +164,23 @@ server { # ============================================================ # ============================================================ -# 可选:本服务未内置登录认证,如需公网访问请在反代加一层防护 +# 可选:本服务未内置登录认证 # +# 管理端建议至少加一层 Basic Auth;软件 API 若不便加认证, +# 请用 IP 白名单把来源限制为客户端所在网段。 +# +# 管理端: # location / { # auth_basic "OpenDance Studio"; # auth_basic_user_file /etc/nginx/.htpasswd; # htpasswd -c 生成 +# proxy_pass http://opendance_admin; +# ... +# } +# +# 软件 API: +# location / { +# allow 192.168.150.0/24; # 客户端所在网段 +# deny all; # proxy_pass http://opendance_api; # ... # } diff --git a/deploy/proxy/web.config.example b/deploy/proxy/web.config.example index 690e636..b4c3e13 100644 --- a/deploy/proxy/web.config.example +++ b/deploy/proxy/web.config.example @@ -16,7 +16,18 @@ 放置位置:IIS 站点根目录下的 web.config - 后端(NestJS)自带管理端页面与 SPA 回退,所以这里只需一条全匹配重写规则, + 后端(NestJS)同时监听两个端口,职责不同: + + 软件 API 端口(PORT,默认 3000)—— 给 Windows/Godot 客户端与 Android App + 只有 /api、/health、/static、/contracts + 管理端端口(ADMIN_PORT,默认 3001)—— 给浏览器 + 管理页面 + 上面全部接口 + + **在 IIS 里建两个站点**,把下面这份 web.config 各放一份,只改 里的端口: + 站点 A(如 dance-api.example.com) → http://127.0.0.1:3000/{R:1} + 站点 B(如 dance-admin.example.com)→ http://127.0.0.1:3001/{R:1} + + 后端自带管理端页面与 SPA 回退,所以每个站点只需一条全匹配重写规则, 不需要在 IIS 上再配置默认文档或 URL Rewrite 的 SPA 回退。 ============================================================ --> @@ -25,7 +36,12 @@ - + @@ -70,8 +86,12 @@