================================================================
OpenDance Studio —— Web 管理服务端 v0.2.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.2.0.tar.gz
  cd opendance-web-v0.2.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              # 168 项单元测试 / 10 个文件（VMD 31、模型压缩包 24、
                            #   模型检查器 25、缩略图 8、骨骼过滤 8、
                            #   预处理 25、init-env 16、admin-port 16、
                            #   access-log 9、VRM 检查器 6）
  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 模型包、或 .pmx/.fbx 单文件模型时**必须**能调用到，
                        否则转换失败（422）
  MODEL_CONVERT_FORMAT  需要转换的模型（PMX/FBX）的交付格式：
                        glb（默认，两个客户端都原生加载）| vrm
                        vrm = 在 GLB 之上补 VRMC_vrm 扩展（humanoid 骨骼 / 表情语义），
                        humanoid 骨骼映射不全时自动回退交付 GLB 并写进 conversion_note。
                        产物一律用 `assimp info` 回读校验
  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                          上传模型 + 来源信息（.glb/.vrm/.gltf
                                               直接导入；.pmx/.fbx 服务端转换；
                                               .zip 自动搜索包内模型）
  POST   /api/models/:id/primary              设为全局默认模型（仅 published 允许，否则 409）
  GET    /api/reviews                         审核流水 / 待审列表
  POST   /api/reviews/:type/:id/approve       审核通过（模型可带 setPrimary:true）
  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 上传）
  **系统以 GLB / VRM 为主力格式**：服务端与客户端都把这两种格式作为一等公民
  （Godot 客户端用内置 GLTFDocument 加载 glTF/GLB/VRM，见
  opendance-windows/src/model/vrm_loader.gd，本次无需改动）。

  glTF Binary  .glb          主力（P0）—— **直接导入**，不重编码
  VRM          .vrm          主力（P0）—— 直接导入；GLB + VRM 扩展
                             （humanoid / 表情 / springBone / meta）
  glTF         .gltf         P1 —— 直接导入（分离的 .bin / 贴图需随 .zip 一起上传）
  MMD 压缩包   .zip          P1 —— **自动搜索包内可用模型**（见下）
  MMD 模型     .pmx          P2（输入）—— 服务端转换，优先 GLB、其次 VRM
  FBX          .fbx          P2（输入）—— 服务端转换，优先 GLB、其次 VRM

  入库后**一律 status=pending（待审核）**：上传不再自动发布、不再自动设为默认，
  必须经审核通过（模型审核面板可勾选「同时设为全局默认模型」）才对客户端可见。

模型压缩包（.zip）自动搜索与转换
  上传的 .zip 不再"只找 .pmx"，而是**自动搜索包内可用模型**，走
  "解压 → 选主模型 → 直接导入或转换 → 回读校验 → 入库"，而不是把压缩包当模型存下来。

    选主模型   优先级 .vrm → .glb → .gltf → .pmx → .fbx；同一优先级内按
               "MODEL_CONVERT_PMX 指名字 → 目录最浅 → 文件最大"。
               命中 .vrm/.glb/.gltf 时**直接导入**（不重编码）。
    转换       只有 .pmx/.fbx 需要转换。链路：assimp export → .glb（首选）；
               失败则 assimp export → .gltf，再由 gltf-packer.ts 自行打包回 GLB；
               MODEL_CONVERT_FORMAT=vrm 时在 GLB 之上补 VRMC_vrm 扩展封装成
               VRM 1.0（humanoid 骨骼不全则回退交付 GLB，缺失骨骼写进说明）。
    校验       每一步产物**必须**用 `assimp info` 回读校验。Assimp 会"写出损坏文件"
               （实测 PMX 导出 FBX 退出码 11、回读报 FBX-DOM 错误），只看退出码
               会把坏文件入库。
    贴图       GLB 产物里 Assimp 会把贴图写成外部 URI（还带反斜杠），服务端把
               压缩包内 / .gltf 同目录的贴图内嵌进 GLB 的二进制块，保证单文件可加载。
    说明       每次尝试写入 models.conversion_note，例如
               `Hatsune Miku.pmx → GLB，已内嵌 2 张贴图`。
    溯源       压缩包原件以 model-archive 资产留存，写入 models.source_asset_id；
               迁移 AddModelConversionFields1789450000000。
    失败       包内没有可用模型、Assimp 不可用、两条路径都失败 → HTTP 422，
               不产生模型记录。

    文件名编码 upload.util.ts 的 decodeUploadName() 修正 multipart 文件名的
               latin1/UTF-8 乱码（中文文件名原先会存成 æ¨¡å_…）。
    管理端     上传框 accept 为 .vrm,.glb,.gltf,.fbx,.zip；预览面板显示「转换来源」、
               「文件 sha256」与面数 / 顶点 / 骨骼分析摘要。
    新增字段   DTO/契约新增 conversionNote、fileSha256、stats、thumbnailUrl。

  实测（Assimp 6.0.4）：
  - 上传含 `Hatsune Miku.pmx` 的 MMD 压缩包 → HTTP 201 / 0.86s、format=glb、
    status=pending、conversionNote=`Hatsune Miku.pmx → GLB，已内嵌 2 张贴图`。
    分析结果：vertices=7155 triangles=9071 materials=3 textures=2 images=2
    nodes=240 bones=238 skins=1 morphTargets=0 humanoidMapped=0/22 drivable=false
    maxBoneDepth=23；missingRequiredBones 为全部 16 根
    （日文 MMD 骨名无法映射到 humanoid，审核界面会直接标红）。
  - 直接上传 .glb → HTTP 201 / 0.36s、conversionNote=`直接导入（GLB，未做重编码）`，
    入库文件 sha256 与源文件**一致**（不重编码，字节级保留）。

模型分析、缩略图与 3D 预览（入库后自动执行，失败不影响入库）
  分析      model-inspector.ts 解析 GLB/VRM：面数 / 顶点 / 网格 / 材质 / 贴图 /
            节点 / 形变目标 / 动画 / 蒙皮骨骼数 / **骨骼树** / humanoid 覆盖 /
            可驱动性（缺哪些必需骨骼）/ warnings，写入 models.stats
            （迁移 AddModelStats1789460000000，新增 models.stats TEXT）。
  缩略图    model-thumbnail.ts 用纯 TS 软件光栅化生成 PNG（读 GLB/glTF → 应用节点
            层级与蒙皮 → 包围盒自动构图 → 透视投影 → z-buffer 逐三角形平面着色 →
            手写 PNG 编码），**零原生依赖**（不需要 canvas / headless-gl / puppeteer）。
            默认 512×512，写入 models.thumbnail_asset_id / thumbnailUrl；
            失败或非 glTF 容器时留空，列表页退化为格式占位图。
            实测：512×512 PNG / 15,043 字节，目视确认为居中的 T-pose 角色图；
            真实 10MB VRM（83 关节、5307 顶点）单张耗时 60–121ms（中位 79ms）。
  预览      管理端 3D 预览放开到 vrm/glb/gltf（.fbx 仍提示「仅归档，请先转换为
            GLB/VRM」）；列表页显示 40px 小缩略图，审核面板显示 116px 缩略图。

审核流程（模型）
  模型上传后为 pending，在管理端「审核」页的**模型审核面板**里查看并决定：

    面板内容   体积 / 格式 / 容器 / VRM 版本 / 面数 / 顶点 / 网格 / 材质 / 贴图 /
               骨骼 / 节点 / 形变目标 / 动画 / humanoid 覆盖（已映射/总数 + 明细）/
               **可驱动性结论**（缺哪些必需骨骼）/ warnings。
    骨骼预览   骨骼详细界面：扁平骨骼树按 depth 缩进、显示骨骼名、是否参与蒙皮、
               humanoid 映射徽章、子节点数；支持按名搜索（保留祖先链）、
               「仅显示蒙皮骨骼」开关、限高滚动 + 只渲染前 400 行（千根骨骼不卡）。
    通过       勾选「同时设为全局默认模型」→ 请求体带 `setPrimary:true`
               （`POST /api/reviews/model/:id/approve`）；此时才写库 is_primary
               并把默认模型身份写进源码 src/config/default-model.ts。
    单独设置   `POST /api/models/:id/primary`（仅 published 允许，否则 409）。
    驳回       必须填写审核意见，否则 400。

默认模型是怎么定的
  GET /api/catalog/export 的 defaultModelId 按以下顺序解析：

    0. 环境变量 DEFAULT_MODEL_ID（且模型存在且已发布）
    1. 源码种子 src/config/default-model.ts 的 DEFAULT_MODEL_SEED——按**上传压缩包的
       sha256** 匹配已发布模型（用户文件永远稳定；不能用转换产物的 sha256 ——
       Assimp 每次导出的字节都略有不同），换库 / 重建库后仍有效
    2. format='vrm' 且 published 且 is_primary=1
    3. 任意 published 的 VRM（id 升序取第一个）
    4. id 最小的已发布模型
    5. 无可用模型 → 客户端用内置占位模型

  源码种子 src/config/default-model.ts 当前为**空种子**（占位：id=null、sha256 空串），
  此时 defaultModelId 直接从第 2 步的 VRM 优先规则开始解析；按下面的方式设定默认
  模型后，服务端会把种子写成真实内容。
  实测（v0.2.0）：`POST /api/reviews/model/22/approve` 带 `setPrimary:true` → 200，
  模型 status=published、isPrimary=true，源码种子写入 sourceSha256=72eb6882…；
  重建后 `GET /api/catalog/export` 的 defaultModelId 由 VRM 变为 **22**
  （库中另有 2 个已发布 VRM）。
  也可手工指定：POST /api/models/:id/primary。
  第 1 步的源码写入由服务端在**审核通过并勾选 setPrimary**（或调用 primary 接口）
  时完成；生产 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
    # 上传 → 服务端转换入库（pending）→ 把身份写进 src/config/default-model.ts

    node scripts/import-default-model.mjs --write-source-only --model-id 22
    # 只根据已入库模型补写源码，不调上传 API

    node scripts/import-default-model.mjs --write-source-only --model-id 22 --dry-run
    # 只打印将写入的内容，不落盘

  说明：--url 默认 http://127.0.0.1:3000，--source-type 默认 other。
  v0.2.0 起上传一律 pending，因此 CLI 上传路径只负责转换入库与写源码种子；
  要让它在 `defaultModelId` 中生效，仍需在管理端**审核通过**（勾选「同时设为全局
  默认模型」）或调用 `POST /api/models/:id/primary`。

支持的动作格式（POST /api/motions 上传 → POST /api/motions/:id/preprocess 预处理）
  BVH          .bvh          内置纯 TS 解析器，保留源帧率
  VMD          .vmd          内置解析（MikuMikuDance），固定 30fps；
                             腿部按「足ＩＫ是否带位移」自动选 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         .zip/.pmx/.fbx → GLB/VRM 转换
  web-server/src/models/gltf-packer.ts           glTF（分离文件）→ GLB 打包
  web-server/src/models/glb-textures.ts          GLB 贴图内嵌
  web-server/src/models/vrm-writer.ts            GLB → VRM 1.0 封装（写 VRMC_vrm 扩展）
  web-server/src/models/model-inspector.ts       模型检查器（面数/骨骼树/humanoid 覆盖）
  web-server/src/models/model-thumbnail.ts       纯 TS 光栅化缩略图（零原生依赖）
  web-server/src/config/default-model.ts         默认模型种子（服务端/CLI 写入）
  web-server/src/migrations/1789460000000-AddModelStats.ts
                                                 models.stats 迁移（审核分析结果）
  web-server/admin/src/components/ModelReviewPanel.tsx
                                                 模型审核面板（含骨骼详细界面）
  web-server/admin/src/lib/bone-tree.ts          骨骼树过滤（搜索保留祖先链）
  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. 上传的模型与动作默认需填写来源五元组并通过审核后才对客户端可见。
   **模型上传后一律 pending**：不再自动发布、不再自动设为默认；默认模型只在
   审核通过时勾选「同时设为全局默认模型」（`setPrimary:true`）或调用
   `POST /api/models/:id/primary`（仅 published）时才设置（见 §五）。
5. 未使用 SMPL 拓扑，统一 MediaPipe 33 点，规避非商用许可限制。

----------------------------------------------------------------
八、第三方开源声明
----------------------------------------------------------------

见 THIRD_PARTY.md，或访问站点 http://<host>/ 的「关于」页面。
