# DSH.md —— opendance-web(OpenDance Studio 项目一:Web 管理服务端) ## 0. 本文件的作用 本文件是 **opendance-web 仓库内**的 DSH 执行约束。顶层说明书见用户提供的 《DSH 开发文档:OpenDance Studio》,二者冲突时以顶层文档为准。 ## 1. 项目定位 Web 管理服务端是另两端(Windows 客户端、Android 辅助 App)的**唯一数据源**与 **契约权威**。它管理歌曲、VRM 模型与动作来源,并输出冻结的 REST / WebSocket / 数据模型契约。 ``` 现有 MySQL 实例(外部,本项目不部署) │ ▼ opendance-web ←── 本仓库 │ 冻结 REST API + 数据模型 + WebSocket 关键点协议 ├──▶ opendance-windows └──▶ opendance-android ``` ## 2. 技术栈 | 组件 | 选型 | |---|---| | 后端 | NestJS 11 | | ORM | TypeORM 0.3 | | 数据库 | **现有 MySQL 兼容实例**(当前部署为 MariaDB 10.11.19),mysql2 驱动 | | 管理前端 | React 18 + Vite + TypeScript | | 3D 预览 | three.js + @pixiv/three-vrm | | 多格式解析 | BVH / VMD 内置纯 TS 解析器;FBX/glTF 走可选外部 Assimp | | 认证 | **本版本不含认证**(内网部署);上线前须补 ≥ T-WEB-011 | ## 3. 单次会话加载范围 允许加载: 1. 本文件 `DSH.md` 2. `project.yaml` 3. 当前 `tasks/xxx.md` 4. 该任务文件中 `load` 列出的文件 5. `contracts/` 中被明确引用的**锚点片段** 禁止加载: - `opendance-windows` / `opendance-android` 的任何源码 - 非当前任务的 `tasks/*.md` - 完整 `contracts/` 目录(只引用锚点) - `node_modules/`、`dist/`、构建产物、二进制、模型文件、上传存储 ## 4. 上下文预算 | 项 | 建议值 | |---|---| | 单任务文件数 | ≤ 6 | | 单任务代码行数 | ≤ 1500 | | 单任务 Token | ≤ 12K | | 契约引用 | 只引用锚点,不加载全文 | ## 5. 目录结构 ``` opendance-web/ ├── project.yaml # 任务清单与状态 ├── DSH.md # 本文件 ├── tasks/ # 单任务说明书(001~010) ├── contracts/ # ★ 跨端契约(权威) │ ├── VERSION # 契约版本(唯一来源) │ ├── rest-api.yaml # OpenAPI 3.0 │ ├── websocket-keypoints.md # 239 字节关键点帧 + 控制消息 │ ├── data-models.md # 资源字段与 VRM 优先规则 │ ├── skeleton-topology.md # MediaPipe 33 点拓扑 + 关节角 │ ├── model-formats.md # 支持格式、预处理产物、上传限制 │ ├── database.md # 现有 MySQL 连接与迁移约定 │ └── 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/ # 单元测试 + 端到端冒烟(含 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 │ ├── ctl.sh # 无 init 系统时的进程控制 │ ├── .env.example │ ├── systemd/opendance-api.service │ ├── openrc/opendance-api │ ├── initd/opendance-api │ └── proxy/ # ★ 反代配置说明(nginx / IIS,仅文档不部署) ├── scripts/package-release.sh # 发布产物打包 └── THIRD_PARTY.md ``` ## 6. 硬性约束 1. **不部署 MySQL**。数据库为部署方提供的现有实例,连接信息只经环境变量注入。 **也不使用 Docker**,**也不部署反向代理**:本端只提供业务服务 (API + 自带的管理端页面),反代由另外的服务负责, 本项目只在 `deploy/proxy/` 下提供 IIS 与 nginx 的配置说明文档。 业务服务监听**两个端口**(软件 API 端口 `PORT` + 管理端端口 `ADMIN_PORT`), 约定见 §9。 2. **`DB_SYNCHRONIZE` 生产必须为 `false`**,所有表结构变更走 migration;每个 migration 必须可逆。 3. **绝不把凭证写入仓库**。`.env` 已在 `.gitignore` 中,只提交 `.env.example`。 4. **来源五元组强制必填**:`source_type` / `source_name` / `source_url` / `license` / `author`。任何资产入库缺一即 400。 5. **契约先行**:改接口先改 `contracts/`,再提升 `contracts/VERSION`,同步 `web-server/src/config/contract-versions.ts`,最后才改实现。 `npm run contracts:check` 在构建前拦截漂移。 6. **不使用 SMPL** 及其衍生拓扑(许可风险),统一 MediaPipe 33 点。 7. **上传文件永不使用原始文件名作为路径**,一律 `sha256` 去重后归档。 ## 7. 每任务的执行步骤 ```bash # 1. 静态与单元校验 cd web-server npm run contracts:check npm run typecheck npm run test # 2. 构建 npm run build # 3. 端到端冒烟(需数据库可连通) npm run start:prod & npm run test:smoke # 4. 数据库迁移 npm run migration:run # 5. 提交 git checkout -b task/T-WEB-0XX git add . && git commit -m "feat(web): " git push origin task/T-WEB-0XX # Gitea 创建 PR → 合并到 dev ``` ## 8. 环境变量 见 `web-server/.env.example` 与 `deploy/.env.example`。生产关键项: ``` 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. 双端口约定(软件 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. 上下文超标时的拆分策略 | 超标原因 | 拆分方式 | |---|---| | 单任务涉及多个端点 | 按端点拆 | | 单文件过大 | 按类/函数拆,先生成接口再分实现 | | 契约全文加载 | 改用锚点引用 | | 需要参考其他项目 | 只读契约,不读源码 | | UI 与逻辑耦合 | UI 任务只调用已定义接口 | | 测试与实现同任务 | 测试单独任务,共享接口摘要 |