Files
opendance-web/README.txt
T
zijunchiang e4351c5062 feat(models): 模型压缩包自动转换 + 默认模型写进源码;首次启动 .env 引导;HOST 默认 0.0.0.0
需求 1:模型上传支持压缩包(MMD 模型包)
- common/zip-reader.ts:纯 TS 极简 ZIP 读取器(EOCD/中央目录、STORE+DEFLATE、
  ZIP64 大小字段、Shift-JIS/GBK 文件名回退、拒绝 zip-slip 与加密条目)
- models/model-convert.ts:解压 → 选主模型(指名字/最浅/最大)→ Assimp 导出 FBX
  → **assimp info 回读校验**(Assimp 对带蒙皮大骨架模型会写出损坏文件:
  实测 714 骨骼 PMX 导出退出码 11、产物回读报 FBX-DOM no Objects dictionary found)
  → 失败自动回退 GLB;两种都失败则 422
- models/glb-textures.ts:把 Assimp 写成外部 URI 的贴图内嵌进 GLB 的 BIN chunk,
  保证模型库里单文件即可加载(否则是白模)
- 溯源:压缩包原件存为 model-archive 资产;models 新增 source_asset_id /
  conversion_note(迁移 1789450000000-AddModelConversionFields,已在真实库上
  验证 up/down 可逆)
- 默认模型:转换成功且 autoDefault 不为 false → is_primary=1 + published,
  并把身份(名称/格式/产物 sha256)写进源码 src/config/default-model.ts;
  resolveDefaultModelId 增加 DEFAULT_MODEL_ID 与源码种子两步(按 sha256 匹配,
  换库仍有效),实测库中有 2 个 VRM 时默认模型仍解析为该 GLB
- scripts/import-default-model.mjs:运维 CLI(上传 zip 并写源码 / --write-source-only)
- 修复 multipart 中文文件名乱码(upload.util.decodeUploadName)
- DTO/契约新增 conversionNote、fileSha256;管理端 accept 增加 .zip 并展示转换来源

需求 2:首次启动 .env 引导
- deploy/init-env.sh:交互式逐项填写 8 个键(回车保留默认、口令不回显不加引号、
  端口校验、mysql2 连通性自检且口令只经环境变量传递、只改这 8 个键并保留模板注释、
  chmod 600、结尾打码摘要);--force/--check/-y/-i/--skip-db-check 等开关;
  非交互(-y、OPENDANCE_NONINTERACTIVE、stdin 关闭)绝不提问、读取带超时
- install.sh 缺 .env 时改为调用引导,失败才 die

需求 3:默认监听 0.0.0.0
- 代码默认值、两份 .env 模板、install.sh、init-env.sh 五处一致,并新增测试守护
  (未设置 HOST 时为 0.0.0.0;两份模板都写着 HOST=0.0.0.0)

契约:rest-api / data-models / model-formats → 0.3.0,database → 0.2.0(另两库已同步)

验证:125 项单测通过(7 文件);契约 6 项与 29 条路由一致;typecheck/build 通过;
真实包端到端 HTTP 201 / 0.89s / format=glb / isPrimary=true / published;
GLB 1,152,464 字节、1 mesh / 240 nodes / 1 skin(238 joints) / 2 张内嵌贴图;
迁移 revert→run 实测可逆且数据无损恢复
2026-09-15 13:55:54 +08:00

411 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================================================================
OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
================================================================
本服务端是 OpenDance Studio 三端中的第一端,负责管理歌曲、VRM 模型与
动作来源,是 Windows 客户端与 Android 辅助 App 的唯一数据源与契约权威。
重要:本服务端 **不部署、不管理 MySQL**。数据库为部署方提供的现有实例,
连接信息通过 .env 注入。
----------------------------------------------------------------
一、部署到 Linux 服务器(业务服务,不含反向代理)
----------------------------------------------------------------
职责边界
本端**只提供业务服务**:NestJS API + 管理端页面。
**反向代理由另外的服务/团队负责**,本项目不安装、不配置任何反代软件,
只在 deploy/proxy/ 下提供 IIS 与 nginx 的配置说明文档。
部署形态
Node 业务服务由系统服务托管,同时监听**两个端口**,职责分开:
软件 API 端口(默认 3000,给 Windows/Godot 客户端与 Android App)
/api/ REST API
/health 健康检查
/static/ 上传文件
/contracts/ 契约快照
该端口**不提供管理页面**,/ 与 /songs 等一律返回 404(有意设计)
管理端端口(默认 3001,给浏览器)
管理页面(本服务自带,含 SPA 深链接回退)+ 上面全部接口
反向代理按用途分别转发两个端口,每个端口各一条全匹配转发规则即可。
前置要求
- Linux 服务器,有 root 权限(不需要 sudo,直接以 root 执行即可)
- Node.js 22 或更高版本
- 进程管理:systemd / OpenRC / SysVinit 均可;**三者都没有也能装**
(自动降级为 nohup 后台进程 + PID 文件 + crontab @reboot 开机自启)
- 反向代理:由另外的服务提供,本项目不要求、也不安装
- 一个可连通的现有 MySQL / MariaDB 实例,并已创建数据库与账号:
CREATE DATABASE IF NOT EXISTS dpendance
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'dpendance'@'%' IDENTIFIED BY '<密码>';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES
ON dpendance.* TO 'dpendance'@'%';
FLUSH PRIVILEGES;
(本项目不会创建数据库本身,也不需要 GRANT OPTION / FILE / SUPER 等管理权限)
部署步骤
tar -xzf opendance-web-v0.1.0.tar.gz
cd opendance-web-v0.1.0
bash deploy/init-env.sh # 首次启动引导:逐项问答生成 deploy/.env(见下)
sudo bash deploy/install.sh
也可跳过引导手工填写:
cp deploy/.env.example deploy/.env
vi deploy/.env # 填写 DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME
脚本会依次:
校验环境与 .env → 创建服务用户与目录 → 复制程序文件 →
安装运行期依赖 → 执行 TypeORM 迁移 →
注册并启动系统服务(systemd / OpenRC / SysVinit / 后台进程)→ 等待健康检查
deploy/.env 初始化引导(deploy/init-env.sh)
缺 deploy/.env 时,install.sh 会先调用本脚本引导填写;引导失败才中止安装
(若安装目录里已有上次安装留下的 .env,则直接复用,不会重复提问)。
也可以单独运行:逐项询问数据库与端口,回车保留默认值,写盘前做必填项与
端口范围校验,并可用项目自带的 mysql2 做一次数据库连通性自检——口令只经
环境变量传给子进程,出错信息与摘要一律显示 ***。
bash deploy/init-env.sh # 交互引导(终端里逐项询问)
bash deploy/init-env.sh --force # 已有 .env 也重新逐项询问
bash deploy/init-env.sh --check # 只校验,不改动、不提问(适合复核)
逐项询问的字段(共 8 个):
DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME / PORT / ADMIN_PORT / HOST
DB_PASS 用 read -s 不回显;所有字段按约定写成 `键=值`(**不加引号**),
因此口令请勿含会破坏 shell 语法的字符(& $ ` \ 空格 ; | < > ( ) !),
脚本检测到会告警。写回只替换或追加这 8 个键、保留模板注释,写完
chmod 600 并回读校验;结尾打印摘要(口令 ***)与本次变更列表。
开关:
--force 已有 .env 也重新逐项询问
--check 只校验,不修改文件、不提问
-y, --yes, --non-interactive
非交互:只校验缺字段与端口,绝不提问
-i, --interactive 即使 stdin 不是 TTY 也强制提问(读取超时仍生效)
--skip-db-check 跳过数据库连通性自检
--dir DIR 等价于 --env-file DIR/.env --example DIR/.env.example
--env-file PATH 指定 .env 路径(默认脚本目录下的 .env)
--example PATH 指定模板路径(默认 .env 同目录的 .env.example)
--timeout SECONDS 非 TTY 下每次读取的超时秒数(默认 10)
-h, --help 显示帮助
环境变量:OPENDANCE_NONINTERACTIVE / OPENDANCE_INTERACTIVE /
OPENDANCE_SKIP_DB_CHECK / OPENDANCE_ENV_FILE /
OPENDANCE_ENV_EXAMPLE / OPENDANCE_PROMPT_TIMEOUT
非交互绝不挂住:stdin 非 TTY 且是 `</dev/null` 或已关闭,或带 -y/--yes/
--non-interactive 时只校验不提问;所有 read 都带超时,EOF 或超时立即
exit 1,不会卡在 CI 或自动化里。
部署完成后(直接访问后端端口,<host> 为服务器 IP)
客户端接口: http://<host>:3000/api
健康检查: http://<host>:3000/health
契约快照: http://<host>:3000/contracts/
管理端首页: http://<host>:3001/
(软件 API 端口上没有管理页面:http://<host>:3000/ 返回 404,属正常)
对外访问
由反向代理**按用途分别转发两个端口**:
浏览器管理界面 → 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
它会打印 .env 里的 HOST/PORT、实际监听地址、回环与对外 IP 自测结果、
80/443 是否有反代、防火墙放行情况,并直接给出修复命令。
可覆盖的安装参数
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
升级
解压新版本后重新执行 sudo bash deploy/install.sh 即可(会覆盖程序文件,
保留 deploy/.env 与上传数据)。
卸载
sudo bash deploy/uninstall.sh # 停服务、禁用站点,保留数据
sudo bash deploy/uninstall.sh --purge # 一并删除程序目录与上传文件
数据库始终由部署方自行管理,卸载脚本不做任何改动。
常用命令(按安装时自动检测到的初始化系统而定)
systemd:
systemctl status opendance-api # 服务状态
journalctl -u opendance-api -f # 实时日志
systemctl restart opendance-api # 重启
OpenRC:
rc-service opendance-api status / restart
tail -f /var/log/opendance-api.log
SysVinit:
/etc/init.d/opendance-api status / restart
无 init 系统(降级模式):
bash /opt/opendance/deploy/ctl.sh status | restart | logs
反代配置:不在本机,无需在此维护
----------------------------------------------------------------
二、本地开发
----------------------------------------------------------------
后端
cd web-server
cp .env.example .env # 填写 DB_*
npm install
npm run migration:run # 首次需要
npm run start:dev # API http://localhost:3000;管理端 http://localhost:3001
管理前端
cd web-server/admin
npm install
npm run dev # http://localhost:5173(/api 与 /health 代理到 3000)
----------------------------------------------------------------
三、验证
----------------------------------------------------------------
cd web-server
npm run contracts:check # 契约版本一致性
npm run typecheck # TypeScript 严格检查
npm run test # 125 项单元测试 / 7 个文件(含 VMD 31、模型压缩包 22、
# init-env 16、预处理 25)
npm run build # 生产构建
npm run start:prod & # 启动
npm run test:smoke # 57 项端到端冒烟(需要数据库可连通)
cd ../admin && npm run build # 管理端构建
----------------------------------------------------------------
四、环境变量
----------------------------------------------------------------
DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME
现有 MySQL 实例连接信息(必填)
DB_SYNCHRONIZE
必须为 false。生产环境启动时若检测到 true 会直接中止启动。
所有表结构变更通过 TypeORM migration 执行。
DB_LOGGING 是否打印 SQL(默认 false)
PORT 软件 API 监听端口(默认 3000)。给 Windows/Godot 客户端
与 Android App 调用,只提供 /api、/health、/static、
/contracts,**不提供管理页面**
ADMIN_PORT 管理端端口(默认 PORT + 1,即 3001)。浏览器访问管理界面
(管理页面 + 上面全部接口);设为与 PORT 相同即为
单端口兼容模式,日志会提示「未分端口」
HOST 监听地址(软件 API 端口),**默认 0.0.0.0**,因此装好即可
被客户端访问(此时请用防火墙限制来源)。反代与后端同机时
建议改为 127.0.0.1,让后端不直接对外暴露
ADMIN_HOST 管理端监听地址(可选,默认跟随 HOST;HOST 未设置时同样
回落 0.0.0.0)。需要接口对外、管理端只对本机时设为
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)。
★ 反向代理层的同名限制必须不小于此值,否则大模型上传
会在代理层被拒(nginx client_max_body_size /
IIS requestLimits),详见 deploy/proxy/README.md
ASSIMP_BIN 可选:Assimp 可执行文件,FBX/glTF 动作预处理需要;
上传 .zip 模型包时**必须**能调用到,否则转换失败(422)
MODEL_CONVERT_FORMAT 模型压缩包首选的目标格式(默认 fbx)。导出后会用
`assimp info` 回读校验,失败自动回退 glb
MODEL_CONVERT_PMX 压缩包内优先选用的 PMX 文件名(默认 Hatsune Miku.pmx,
大小写不敏感);找不到时取目录最浅、其次文件最大者
MODEL_CONVERT_TIMEOUT_MS
单次转换超时(毫秒,默认 180000)
DEFAULT_MODEL_ID 可选:显式指定默认模型 id;不设置时按源码种子
src/config/default-model.ts 匹配,再回落到 VRM 优先规则
CONTRACTS_DIR 可选:契约目录,设置后 /health 反映实时契约版本
(原生部署默认指向 /opt/opendance/contracts)
----------------------------------------------------------------
五、主要 API
----------------------------------------------------------------
GET /health 健康检查(含数据库连通性)
GET /api/songs 曲目列表(分页 / 检索)
POST /api/songs 新建曲目
GET /api/songs/:id 曲目详情
PATCH /api/songs/:id 更新曲目
DELETE /api/songs/:id 删除曲目(软删除)
POST /api/songs/:id/audio 上传音频
GET /api/motions 动作列表
POST /api/motions 上传动作 + 来源信息
POST /api/motions/:id/preprocess BVH/VMD/FBX → MediaPipe33 关键点
GET /api/motions/:id/keypoints 下载关键点序列(NDJSON)
GET /api/models 模型列表
POST /api/models 上传模型 + 来源信息(VRM/GLB/FBX,
或含 .pmx 的 .zip 模型包)
POST /api/models/:id/primary 设为全局默认模型
GET /api/reviews 审核流水 / 待审列表
POST /api/reviews/:type/:id/approve 审核通过
POST /api/reviews/:type/:id/reject 审核驳回(必须填意见)
GET /api/catalog/export 客户端一次性拉取已发布目录
GET /api/catalog/version 契约版本 + 目录指纹
完整契约(OpenAPI 3.0)见 contracts/rest-api.yaml。
上传文件通过 /static/<kind>/<yyyy>/<mm>/<sha256>.ext 访问,
相同内容只落盘一次(sha256 去重)。
支持的模型格式(POST /api/models 上传)
VRM .vrm 首选(P0);glTF 2.0 二进制 + VRM 扩展
glTF Binary .glb P1;需含骨骼
glTF .gltf P2;多文件,建议打包后上传
FBX .fbx P2;需 Assimp,管理端仅归档
MMD 压缩包 .zip P1;内含 .pmx(+ textures/),服务端自动解压转换
模型压缩包(.zip)自动转换
上传的 .zip 视为 MMD 模型包,走"解压 → 选主模型 → 转换 → 回读校验 → 入库",
而不是把压缩包当模型文件存下来。
选主模型 优先 MODEL_CONVERT_PMX 指定的文件名(默认 Hatsune Miku.pmx,
大小写不敏感);否则取目录最浅、其次文件最大者。只认 .pmx。
转换 先试 MODEL_CONVERT_FORMAT(默认 fbx),调 ASSIMP_BIN 导出。
校验 导出后**必须**用 `assimp info` 回读校验。Assimp 的 FBX 导出器
对带蒙皮的大骨架模型会"写出损坏文件却返回非 0 退出码",只信
退出码会把坏文件入库。
回退 FBX 失败(导出失败或回读失败)→ 自动改用 GLB,两次尝试写入
models.conversion_note。
贴图 GLB 产物里 Assimp 会把贴图写成外部 URI(还带反斜杠),服务端
把压缩包内的贴图内嵌进 GLB 的二进制块,保证单文件可加载。
默认模型 转换成功且 autoDefault 不为 false(默认 true)→ is_primary=1 +
status=published,并把身份写进源码 src/config/default-model.ts。
溯源 压缩包原件以 model-archive 资产留存,写入 models.source_asset_id;
转换说明写入 models.conversion_note(迁移 AddModelConversionFields)。
失败 压缩包内没有 .pmx、Assimp 不可用、两种格式都失败 → HTTP 422,
不产生模型记录。
文件名编码 upload.util.ts 的 decodeUploadName() 修正 multipart 文件名的
latin1/UTF-8 乱码(中文文件名原先会存成 模å_…)。
管理端 上传框 accept 增加 .zip,提示会说明"会自动转换并设为默认";
预览面板新增「转换来源」与「文件 sha256」。
新增字段 DTO/契约新增 conversionNote、fileSha256。
实测:上传 示例文件/模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip
(1,093,100 字节)→ HTTP 201、0.89s、format=glb、isPrimary=true、
status=published。服务端日志显示 Assimp 6.0.4 导出 FBX 失败(退出码 11),
回退 GLB 后回读通过:1,152,464 字节、238 关节、已内嵌 2 张贴图,
conversionNote = `Hatsune Miku.pmx → GLB,已内嵌 2 张贴图`。
默认模型是怎么定的
GET /api/catalog/export 的 defaultModelId 按以下顺序解析:
0. 环境变量 DEFAULT_MODEL_ID(且模型存在且已发布)
1. 源码种子 src/config/default-model.ts 的 DEFAULT_MODEL_SEED——按转换产物
sha256 匹配已发布模型,换库 / 重建库后仍有效
2. format='vrm' 且 published 且 is_primary=1
3. 任意 published 的 VRM(id 升序取第一个)
4. id 最小的已发布模型
5. 无可用模型 → 客户端用内置占位模型
当前种子指向 id=16、name=`初音未来 Hatsune Miku`、format=glb、
sha256=`6d0ab0dce97c43dd7de84aba29b09f3b64f2953722d31b5421a39189ff2a8464`。
实测:库里有 2 个 VRM 的情况下,defaultModelId 仍解析为 16。
也可手工指定:POST /api/models/:id/primary。
第 1 步的源码写入通常由服务端在转换成功时完成;生产 systemd 加固
(ProtectSystem=strict)下服务进程写不了程序目录,可改用下面的 CLI 补写。
导入 / 补写默认模型(web-server/scripts/import-default-model.mjs)
需先 `npm run build`(脚本复用编译产物 dist/models/default-model-source.js)。
cd web-server
node scripts/import-default-model.mjs 模型.zip \
--url http://127.0.0.1:3000 \
--name "初音未来 Hatsune Miku" --author "Boku Wa Luka" \
--license "MMD 模型使用条款(见压缩包内说明)" \
--source-name "模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip" \
--source-url "本地文件:模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip" \
--source-type other
# 上传 → 服务端转换入库 → 设为默认 → 把身份写进 src/config/default-model.ts
node scripts/import-default-model.mjs --write-source-only --model-id 16
# 只根据已入库模型补写源码,不调上传 API
node scripts/import-default-model.mjs --write-source-only --model-id 16 --dry-run
# 只打印将写入的内容,不落盘
说明:--url 默认 http://127.0.0.1:3000,--source-type 默认 other;
上传时脚本固定带 autoDefault=true。
支持的动作格式(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 走正式通路。
----------------------------------------------------------------
六、目录结构
----------------------------------------------------------------
project.yaml 任务清单与状态
DSH.md 本项目 DSH 执行约束
tasks/ 单任务说明书(T-WEB-001 ~ 010)
contracts/ 跨端契约(权威,改接口先改这里)
web-server/ NestJS 服务端源码
web-server/admin/ React 管理端
web-server/src/common/zip-reader.ts 纯 TS ZIP 读取器
web-server/src/models/model-convert.ts 压缩包 → FBX/GLB 转换
web-server/src/models/glb-textures.ts GLB 贴图内嵌
web-server/src/config/default-model.ts 默认模型种子(服务端/CLI 写入)
web-server/scripts/import-default-model.mjs 默认模型导入 / 补写 CLI
deploy/ 业务服务部署(系统服务,不含 MySQL、不含反向代理)
deploy/init-env.sh 首次启动的 .env 交互引导(install.sh 会调用)
deploy/proxy/ 反向代理配置说明(nginx / IIS,仅供参考不部署)
THIRD_PARTY.md 第三方开源项目声明
----------------------------------------------------------------
七、安全提示
----------------------------------------------------------------
1. .env 已被 .gitignore 忽略,禁止提交;仓库中只保留 .env.example。
2. 数据库账号使用最小权限,不授予 GRANT OPTION / FILE / SUPER。
3. 本版本未内置管理端登录认证,请部署在内网,或在反向代理层加 Basic Auth / SSO。
4. 上传的模型与动作默认需填写来源五元组并通过审核后才对客户端可见;上传 .zip
模型包且未关闭 autoDefault 时,转换成功会直接设为已发布的默认模型(见 §五)。
5. 未使用 SMPL 拓扑,统一 MediaPipe 33 点,规避非商用许可限制。
----------------------------------------------------------------
八、第三方开源声明
----------------------------------------------------------------
见 THIRD_PARTY.md,或访问站点 http://<host>/ 的「关于」页面。