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 实测可逆且数据无损恢复
This commit is contained in:
2026-09-15 13:55:54 +08:00
parent bba5bcf59e
commit e4351c5062
34 changed files with 3204 additions and 65 deletions
+151 -9
View File
@@ -52,15 +52,57 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
部署步骤
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
sudo bash deploy/install.sh
脚本会依次:
校验环境与 .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
@@ -145,7 +187,8 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
cd web-server
npm run contracts:check # 契约版本一致性
npm run typecheck # TypeScript 严格检查
npm run test # 85 项单元测试(含 31 项 VMD)
npm run test # 125 项单元测试 / 7 个文件(含 VMD 31、模型压缩包 22、
# init-env 16、预处理 25)
npm run build # 生产构建
npm run start:prod & # 启动
npm run test:smoke # 57 项端到端冒烟(需要数据库可连通)
@@ -170,10 +213,12 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
ADMIN_PORT 管理端端口(默认 PORT + 1,即 3001)。浏览器访问管理界面
(管理页面 + 上面全部接口);设为与 PORT 相同即为
单端口兼容模式,日志会提示「未分端口」
HOST 监听地址(软件 API 端口)。反代与后端同机用 127.0.0.1
(推荐);反代在别的机器用 0.0.0.0,并用防火墙限制来源
ADMIN_HOST 管理端监听地址(可选,默认跟随 HOST)。需要接口对外、
管理端只对本机时设为 127.0.0.1
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)
@@ -182,7 +227,16 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
★ 反向代理层的同名限制必须不小于此值,否则大模型上传
会在代理层被拒(nginx client_max_body_size /
IIS requestLimits),详见 deploy/proxy/README.md
ASSIMP_BIN 可选:Assimp 可执行文件,FBX/glTF 动作预处理需要
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)
@@ -202,7 +256,8 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
POST /api/motions/:id/preprocess BVH/VMD/FBX → MediaPipe33 关键点
GET /api/motions/:id/keypoints 下载关键点序列(NDJSON)
GET /api/models 模型列表
POST /api/models 上传模型 + 来源信息(VRM 优先)
POST /api/models 上传模型 + 来源信息(VRM/GLB/FBX,
或含 .pmx 的 .zip 模型包)
POST /api/models/:id/primary 设为全局默认模型
GET /api/reviews 审核流水 / 待审列表
POST /api/reviews/:type/:id/approve 审核通过
@@ -215,6 +270,86 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明
上传文件通过 /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;
@@ -247,7 +382,13 @@ VMD 动作上传前后自查
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 第三方开源项目声明
@@ -258,7 +399,8 @@ VMD 动作上传前后自查
1. .env 已被 .gitignore 忽略,禁止提交;仓库中只保留 .env.example。
2. 数据库账号使用最小权限,不授予 GRANT OPTION / FILE / SUPER。
3. 本版本未内置管理端登录认证,请部署在内网,或在反向代理层加 Basic Auth / SSO。
4. 上传的模型与动作必须填写来源五元组并通过审核后才对客户端可见。
4. 上传的模型与动作默认需填写来源五元组并通过审核后才对客户端可见;上传 .zip
模型包且未关闭 autoDefault 时,转换成功会直接设为已发布的默认模型(见 §五)。
5. 未使用 SMPL 拓扑,统一 MediaPipe 33 点,规避非商用许可限制。
----------------------------------------------------------------