release(v0.7.1): 契约 websocket-keypoints 0.2.0 + 补齐场景规格与任务文档
契约变更(T-WEB-016,与 Android T-AND-011「两端一起改」配套)
- websocket-keypoints 0.1.0 -> 0.2.0,均为向后兼容(子协议名与二进制帧
version 均不变,只新增可选控制字段与一种新控制类型):
1.1 保活:接收端必须声明 silentTimeoutMs 与 heartbeatIntervalMs,且心跳必须
小于静默超时的一半。根因是客户端只能用 OkHttp 的协议层 ping,而它由
底层库自动应答、**不暴露给应用层**,接收端根本看不到,无法续命;
3.1 hello.deviceId(该安装实例的稳定标识,不使用任何硬件序列号);
3.2/3.3 welcome/config 携带保活字段;
3.6 应用层 ping/pong(回显 sentAtMs,pong 另带 serverAtMs 供算 RTT);
5 设备去重:同一 deviceId 只保留最新连接,旧连接以 1000 关闭
(刻意用 1000:对端据 §7 不会重连,否则形成"挤掉-重连-再挤掉"循环);
7 关闭码与重连策略:1000/1001/1002 与主动停止不重连,其余重连;
7.1 退避参数冻结:min(500ms*2^(n-1), 15000ms) + ±20% 抖动;
8 连接状态机(两端同名,日志可直接对照)。
- contracts/VERSION 与 src/config/contract-versions.ts 同步到 0.2.0。
补齐 rest-api.yaml 漏写的场景结构(只补规格,不改线格式,版本号不变)
- CatalogBundle 增加 scenes[](标注 0.5.0 起下发);
- 新增 Scene / SceneConfig / Vec3 schemas。catalog/export 自 v0.7.0 起就返回
scenes[],data-models 7 也已冻结,但规格漏写会让两端对字段边界
(如 0~1、±200)的理解不一致。
deploy/proxy/README.md 新增第九节:Gitea 站点的附件上传上限
- 与一~五节的 Web 服务反代是**两个不同站点**;实测 30.5 MB / 40 MB 上传返回
HTTP 413,根因是 IIS 默认 maxAllowedContentLength = 30,000,000 字节;
- 给出放宽到 256 MB 的 IIS / ARR / Gitea app.ini / nginx 四处配置与复验命令;
- 这解释了 Windows 端 v0.1.0 为何被拆成 zip.part0/1/2(交付缺陷)。
文档与脚本
- 补齐 tasks/011~016 与 project.yaml 条目(T-WEB-011~015 此前已在代码中落地
却缺任务说明书);project.yaml 的 contracts_version 由单个 0.4.0 改为六项字典;
- scripts/package-release.sh 的版本号改为从 web-server/package.json 读取
(与 GET /health 的 version、管理端左下角展示同源),并与 project.yaml 交叉校验;
同时补上 SHA256SUMS 与自校验(此前是手工生成的)。
验证:npm run verify 通过(contracts:check 六项一致 + 35 条路由一致、
typecheck、246 项测试、nest build);产物 opendance-web-v0.7.1.tar.gz
sha256 27f92cfb125bbcf29077d4ad90c44720cbca46d23ddef12cd7826d73bd8865e2。
This commit is contained in:
@@ -10,7 +10,15 @@ OpenDance Studio **阶段一**: Web 管理服务端(NestJS + TypeORM + 现有 My
|
||||
|
||||
## 状态
|
||||
|
||||
**阶段一已完成**: T-WEB-001 -> T-WEB-010 全部落地,当前版本 **v0.7.0**(契约 `rest-api 0.5.0`、`data-models 0.7.0`、`model-formats 0.5.0`、`skeleton-topology 0.2.0`、`database 0.4.0`)。
|
||||
**阶段一已完成**: T-WEB-001 -> T-WEB-016 全部落地,当前版本 **v0.7.1**(契约 `rest-api 0.5.0`、`websocket-keypoints 0.2.0`、`data-models 0.7.0`、`model-formats 0.5.0`、`skeleton-topology 0.2.0`、`database 0.4.0`)。
|
||||
|
||||
v0.7.1 修订(**契约补齐 + 文档补齐**,契约 `websocket-keypoints` 0.1.0 -> 0.2.0):
|
||||
|
||||
1. **补齐 `rest-api.yaml` 里漏写的场景结构**:`catalog/export` 自 v0.7.0 起就返回 `scenes[]`,`data-models.md` 7 也已冻结,但 `CatalogBundle` 漏了这个字段、`components` 里也没有 `Scene`/`SceneConfig`。本次只补规格、**不改线格式**,故六个契约版本号不变(rest-api 仍 0.5.0);
|
||||
2. **契约 `websocket-keypoints` -> 0.2.0**(T-WEB-016,与 Android `T-AND-011` 的"两端一起改"配套):新增 1.1 **保活**(接收端必须声明 `silentTimeoutMs`/`heartbeatIntervalMs`,且**心跳 < 静默超时的一半**)、3.1 `hello.deviceId`、3.2/3.3 welcome/config 携带保活字段、3.6 应用层 `ping`/`pong`、5 **设备去重**、7 **关闭码与重连策略**、7.1 退避与 ±20% 抖动、8 **连接状态机**(两端同名)。子协议名与二进制帧 `version` 均不变,**向后兼容**;
|
||||
3. **`deploy/proxy/README.md` 新增第九节**:托管 Gitea 的 IIS 站点附件上传上限(与一~五节的 Web 服务反代是**两个不同站点**)。实测 30.5 MB / 40 MB 返回 HTTP 413,根因是 IIS 默认 `maxAllowedContentLength = 30,000,000` 字节;给出放宽到 256 MB 的 IIS/ARR/Gitea `app.ini`/nginx 四处配置与复验命令 —— 这正是 Windows 端 v0.1.0 被拆成 `zip.part0/1/2` 的根因;
|
||||
4. **补齐 `tasks/011` ~ `tasks/016`**:此前 T-WEB-011~015 已在代码中落地但缺少任务说明书与 `project.yaml` 条目,本次一并补上(T-WEB-011 版本号展示 / 012 VRM 优先 / 013 动作标准与骨骼重绑定 / 014 动作预览 / 015 场景管理 / 016 契约 0.2.0);
|
||||
5. 规模 **18 文件 / 246 项**测试,`contracts:check` 通过、`check-routes` 35 条路由一致。
|
||||
|
||||
v0.7.0 修订(**管理端场景页**,补齐场景端到端可用):
|
||||
|
||||
@@ -115,15 +123,15 @@ v0.2.0 主要变更:
|
||||
3. 取得发布包并校验(地址用你们自己的制品库替换; 下面是本仓库的发布地址)
|
||||
|
||||
```bash
|
||||
curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.7.0/opendance-web-v0.7.0.tar.gz
|
||||
curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.7.0/SHA256SUMS
|
||||
curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.7.1/opendance-web-v0.7.1.tar.gz
|
||||
curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.7.1/SHA256SUMS
|
||||
sha256sum -c SHA256SUMS # 期望输出 "OK"
|
||||
```
|
||||
|
||||
4. 解压并生成配置(交互式引导,逐项问答,口令不回显)
|
||||
|
||||
```bash
|
||||
tar -xzf opendance-web-v0.7.0.tar.gz && cd opendance-web-v0.7.0
|
||||
tar -xzf opendance-web-v0.7.1.tar.gz && cd opendance-web-v0.7.1
|
||||
bash deploy/init-env.sh
|
||||
```
|
||||
|
||||
@@ -193,8 +201,8 @@ FLUSH PRIVILEGES;
|
||||
### 5. 部署步骤
|
||||
|
||||
```bash
|
||||
tar -xzf opendance-web-v0.7.0.tar.gz
|
||||
cd opendance-web-v0.7.0
|
||||
tar -xzf opendance-web-v0.7.1.tar.gz
|
||||
cd opendance-web-v0.7.1
|
||||
bash deploy/init-env.sh # 首次启动引导: 逐项问答生成 deploy/.env(见下)
|
||||
bash deploy/install.sh # 已以 root 登录, 不需要 sudo
|
||||
```
|
||||
|
||||
+21
-5
@@ -1,5 +1,5 @@
|
||||
================================================================
|
||||
OpenDance Studio -- Web 管理服务端 v0.7.0 使用说明
|
||||
OpenDance Studio -- Web 管理服务端 v0.7.1 使用说明
|
||||
================================================================
|
||||
|
||||
本服务端是 OpenDance Studio 三端中的第一端, 负责管理歌曲、VRM 模型与
|
||||
@@ -8,6 +8,22 @@ OpenDance Studio -- Web 管理服务端 v0.7.0 使用说明
|
||||
重要: 本服务端 不部署、不管理 MySQL。数据库为部署方提供的现有实例,
|
||||
连接信息通过 .env 注入。
|
||||
|
||||
v0.7.1 修订 - 契约补齐 + 文档补齐(契约 websocket-keypoints 0.1.0 -> 0.2.0)
|
||||
1. 补齐 rest-api.yaml 里漏写的场景结构(CatalogBundle.scenes[] + Scene +
|
||||
SceneConfig): catalog/export 自 v0.7.0 起就返回 scenes[],规格漏写会导致
|
||||
两端对字段边界的理解不一致。只补规格、不改线格式,六个契约版本号不变。
|
||||
2. 契约 websocket-keypoints -> 0.2.0(与 Android T-AND-011 配套):
|
||||
1.1 保活(接收端声明 silentTimeoutMs/heartbeatIntervalMs,心跳必须小于
|
||||
静默超时的一半)、3.1 hello.deviceId、3.6 应用层 ping/pong、
|
||||
5 设备去重、7 关闭码与重连策略、7.1 退避与 ±20% 抖动、8 连接状态机。
|
||||
子协议名与帧 version 均不变,向后兼容。
|
||||
3. deploy/proxy/README.md 新增第九节: 托管 Gitea 的 IIS 站点附件上传上限
|
||||
(与一~五节的 Web 服务反代是两个不同站点)。实测 30.5/40 MB 返回 HTTP 413,
|
||||
根因是 IIS 默认 maxAllowedContentLength = 30,000,000 字节;给出放宽到
|
||||
256 MB 的配置与复验命令。这是 Windows 端 v0.1.0 被拆成 zip.part0/1/2 的根因。
|
||||
4. 补齐 tasks/011 ~ 016 与 project.yaml 条目(此前已落地但缺任务说明书)。
|
||||
5. 规模 18 文件 / 246 项测试; contracts:check 通过, 35 条路由一致。
|
||||
|
||||
v0.7.0 修订 - 管理端场景页(补齐场景端到端可用)
|
||||
1. 新增「场景管理」页(/scenes): 列表(40px 缩略图 + 物理摘要 + 状态)、
|
||||
上传(GLB 文件 + 名称 + 来源五元组 + 物理参数: 重力/风/阵风/地面/碰撞体/环境光)、
|
||||
@@ -143,11 +159,11 @@ v0.2.1 修订
|
||||
2. 准备一个现有的 MySQL / MariaDB 实例, 并建好库与账号(见下方"前置要求")
|
||||
本项目不会创建数据库本身
|
||||
3. 取得发布包并校验(地址用你们自己的制品库替换)
|
||||
curl -LO https://<制品库地址>/opendance-web-v0.7.0.tar.gz
|
||||
curl -LO https://<制品库地址>/opendance-web-v0.7.1.tar.gz
|
||||
curl -LO https://<制品库地址>/SHA256SUMS
|
||||
sha256sum -c SHA256SUMS # 期望输出 "OK"
|
||||
4. 解压并生成配置(交互式引导, 逐项问答, 口令不回显)
|
||||
tar -xzf opendance-web-v0.7.0.tar.gz && cd opendance-web-v0.7.0
|
||||
tar -xzf opendance-web-v0.7.1.tar.gz && cd opendance-web-v0.7.1
|
||||
bash deploy/init-env.sh
|
||||
5. 安装(建用户与目录 -> 装依赖 -> 迁移 -> 注册系统服务 -> 等健康检查)
|
||||
bash deploy/install.sh
|
||||
@@ -205,8 +221,8 @@ v0.2.1 修订
|
||||
(本项目不会创建数据库本身, 也不需要 GRANT OPTION / FILE / SUPER 等管理权限)
|
||||
|
||||
部署步骤
|
||||
tar -xzf opendance-web-v0.7.0.tar.gz
|
||||
cd opendance-web-v0.7.0
|
||||
tar -xzf opendance-web-v0.7.1.tar.gz
|
||||
cd opendance-web-v0.7.1
|
||||
bash deploy/init-env.sh # 首次启动引导: 逐项问答生成 deploy/.env(见下)
|
||||
bash deploy/install.sh # 已以 root 登录, 不需要 sudo
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# OpenDance Studio 契约版本
|
||||
# 规则:任何字段级变更必须提升对应条目版本,并回到 opendance-web 修改后再向另两端建迁移任务。
|
||||
rest-api: 0.5.0
|
||||
websocket-keypoints: 0.1.0
|
||||
websocket-keypoints: 0.2.0
|
||||
data-models: 0.7.0
|
||||
skeleton-topology: 0.2.0
|
||||
model-formats: 0.5.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# contracts/websocket-keypoints.md - 关键点传输契约 v0.1.0
|
||||
# contracts/websocket-keypoints.md - 关键点传输契约 v0.2.0
|
||||
|
||||
Android 辅助 App(发送端)-> Windows 客户端(接收端)。
|
||||
服务端不参与实时链路,但 T-WEB-006 预处理产出的序列必须与此结构同构。
|
||||
@@ -14,6 +14,22 @@ ws://<windows-host>:8765/pose
|
||||
- 二进制帧传输关键点,文本帧传输控制消息(JSON)。
|
||||
- 默认 30 FPS;发送端允许丢帧,接收端必须按 `frame_id` 检测丢包并插值补齐。
|
||||
|
||||
### 1.1 保活(v0.2.0 新增,向后兼容)
|
||||
|
||||
接收端有**静默超时**:超过 `silentTimeoutMs` 没有收到任何数据帧或文本帧即断开
|
||||
(`close(1001, "timeout")`)。为此:
|
||||
|
||||
- 接收端必须在 `welcome`(以及需要变更时的 `config`)中声明
|
||||
`silentTimeoutMs` 与期望的 `heartbeatIntervalMs`;
|
||||
- 发送端按 `heartbeatIntervalMs` 周期发送 `ping`(见 §3.6),
|
||||
且 **`heartbeatIntervalMs` 必须小于 `silentTimeoutMs` 的一半**;
|
||||
- 发送端每帧关键点与每秒 `stats` 同样算作活动,`ping` 只是兜底,
|
||||
保证「画面里没有人 / 相机暂停」时连接不会被误判掉线。
|
||||
|
||||
> 子协议名与二进制帧 `version` 均**不变**:本版本只**新增可选**控制字段与
|
||||
> 一种新的控制类型,遵循 §5 的兼容规则;仅实现 v0.1.0 的一端仍可正常互通
|
||||
> (它不发 `ping`、不读 `silentTimeoutMs`,但每帧与每秒 stats 已足够保活)。
|
||||
|
||||
## 2. 二进制帧:239 字节(冻结)
|
||||
|
||||
**小端序(little-endian)**。
|
||||
@@ -76,26 +92,54 @@ func decode(buf: PackedByteArray) -> Dictionary:
|
||||
{
|
||||
"type": "hello",
|
||||
"protocol": "opendance.pose.v1",
|
||||
"deviceId": "a1b2c3d4e5f60718",
|
||||
"device": "Pixel 8",
|
||||
"appVersion": "0.1.0",
|
||||
"appVersion": "0.2.0",
|
||||
"cameraFacing": "front",
|
||||
"targetFps": 30,
|
||||
"resolution": "640x480"
|
||||
}
|
||||
```
|
||||
|
||||
- `deviceId`(v0.2.0 新增,**可选但强烈建议**):该安装实例的稳定标识
|
||||
(建议 16 位十六进制,取自 `ANDROID_ID` 的哈希,不使用任何硬件序列号)。
|
||||
接收端**必须按 `deviceId` 去重**:同一 `deviceId` 的新连接替换旧连接,
|
||||
设备数按去重后计数(否则一次「反复重连」会被显示成多台设备)。
|
||||
未提供 `deviceId` 时接收端退回按连接计数。
|
||||
|
||||
### 3.2 `welcome` - Windows -> Android
|
||||
|
||||
```json
|
||||
{ "type": "welcome", "protocol": "opendance.pose.v1", "sessionId": "uuid", "expectedFps": 30 }
|
||||
{
|
||||
"type": "welcome",
|
||||
"protocol": "opendance.pose.v1",
|
||||
"sessionId": "uuid",
|
||||
"expectedFps": 30,
|
||||
"silentTimeoutMs": 15000,
|
||||
"heartbeatIntervalMs": 5000
|
||||
}
|
||||
```
|
||||
|
||||
- `silentTimeoutMs` / `heartbeatIntervalMs`(v0.2.0 新增):见 §1.1。
|
||||
发送端若未读到这两个字段,应使用保守默认值(超时 5000、心跳 1000)。
|
||||
|
||||
### 3.3 `config` - Windows -> Android,运行中调整
|
||||
|
||||
```json
|
||||
{ "type": "config", "mirror": true, "targetFps": 30, "upperBodyOnly": false, "resolution": "640x480" }
|
||||
{
|
||||
"type": "config",
|
||||
"mirror": true,
|
||||
"targetFps": 30,
|
||||
"upperBodyOnly": false,
|
||||
"resolution": "640x480",
|
||||
"silentTimeoutMs": 15000,
|
||||
"heartbeatIntervalMs": 5000
|
||||
}
|
||||
```
|
||||
|
||||
- `silentTimeoutMs` / `heartbeatIntervalMs` 为 v0.2.0 新增的可选字段,
|
||||
接收端在运行中调整保活参数时一并下发。
|
||||
|
||||
### 3.4 `stats` - Android -> Windows,每秒一次
|
||||
|
||||
```json
|
||||
@@ -108,6 +152,31 @@ func decode(buf: PackedByteArray) -> Dictionary:
|
||||
{ "type": "bye", "reason": "user_stop" }
|
||||
```
|
||||
|
||||
`bye` 表示**主动**结束:发出方在发送后立即关闭连接,接收方据此关闭且
|
||||
**不得**自动重连(见 §6)。
|
||||
|
||||
### 3.6 `ping` / `pong` - 保活(v0.2.0 新增)
|
||||
|
||||
Android -> Windows:
|
||||
|
||||
```json
|
||||
{ "type": "ping", "sentAtMs": 1730000000000 }
|
||||
```
|
||||
|
||||
Windows -> Android(可选回复,用于 RTT):
|
||||
|
||||
```json
|
||||
{ "type": "pong", "sentAtMs": 1730000000000, "serverAtMs": 1730000000042 }
|
||||
```
|
||||
|
||||
- `sentAtMs` 为发送方的本地毫秒时间戳,原样回传;发送端用
|
||||
`now - sentAtMs` 估算 RTT。
|
||||
- 接收端收到任何一帧(二进制或文本)都算活动,`ping` 无需额外应答即可保活;
|
||||
`pong` 只用于诊断。
|
||||
- **不要**依赖 WebSocket 协议层的 ping 帧(`Sec-WebSocket-Ping`):它由底层
|
||||
库自动处理,**不会**暴露给应用层,因此不能被接收端的静默超时逻辑看到。
|
||||
这也是必须定义应用层 `ping` 的原因。
|
||||
|
||||
## 4. JSON 调试帧(可选,仅开发期)
|
||||
|
||||
```json
|
||||
@@ -123,9 +192,68 @@ func decode(buf: PackedByteArray) -> Dictionary:
|
||||
|
||||
生产链路必须使用二进制帧;JSON 帧仅用于联调,接收端可选择性支持。
|
||||
|
||||
## 5. 兼容性规则
|
||||
## 5. 设备去重(v0.2.0 新增)
|
||||
|
||||
接收端按 §3.1 的 `deviceId` 去重:
|
||||
|
||||
| 情况 | 行为 |
|
||||
|--|--|
|
||||
| 新连接的 `deviceId` 已存在 | 关闭旧连接(`close(1000, "replaced by new connection")`),保留新连接;设备数不变 |
|
||||
| `deviceId` 缺失或为空 | 不做去重,按连接计数 |
|
||||
| 同一 `deviceId` 的旧连接已断开 | 正常替换,不影响设备数 |
|
||||
|
||||
这样「手机闪断后立刻重连」不会在接收端显示成两台设备,也不会让
|
||||
「已连接 N 台」抖动。
|
||||
|
||||
## 6. 兼容性规则
|
||||
|
||||
- `version` 不同的大版本接收端必须拒绝并在 `bye.reason` 中说明。
|
||||
- `flags` 新增位不得改变既有位语义;旧接收端必须忽略未知位。
|
||||
- 33 点顺序一旦冻结不得插入或重排;新增点只能追加新 `version`。
|
||||
- 服务端 `motions.keypoint_format` 字段取值 `mediapipe33@0.1.0` 对应本契约。
|
||||
|
||||
## 7. 关闭码与重连策略(v0.2.0 新增,两端必须一致)
|
||||
|
||||
| 关闭码 / 情形 | 含义 | 发送端是否自动重连 |
|
||||
|--|--|--|
|
||||
| `1000` 正常关闭 | 任一方主动结束(含收到 `bye`) | **否** |
|
||||
| `1001` going away | 接收端主动关闭(服务重启、`stop()`、静默超时踢人) | **否**(见下方说明) |
|
||||
| `1002` 协议错误 | 子协议 / 协议版本不匹配 | **否**(重连也不会成功) |
|
||||
| 连接失败 / 超时 / `1006` 异常断开 | 网络抖动、对端进程退出、Wi-Fi 切换 | **是** |
|
||||
| 客户端主动 `disconnect()` | 用户点「结束推流」 | **否** |
|
||||
|
||||
- 静默超时由接收端发 `close(1001, "timeout")`;正常情况下发送端按 §1.1 保活,
|
||||
**不应**触发它。若仍频繁触发,属于保活参数或实现缺陷,需按 §7 排查,
|
||||
而不是靠重连掩盖。
|
||||
- 接收端因「同一 `deviceId` 被新连接替换」关闭旧连接时用 `1000`,
|
||||
旧连接不重连。
|
||||
|
||||
### 7.1 退避参数(冻结)
|
||||
|
||||
```
|
||||
第 n 次重连等待 = min(500ms * 2^(n-1), 15000ms) * (1 ± 20% 抖动)
|
||||
即 0.5s → 1s → 2s → 4s → 8s → 15s(封顶),每次叠加 ±20% 抖动
|
||||
连接成功后计数清零
|
||||
```
|
||||
|
||||
## 8. 连接状态机(v0.2.0 新增,两端同名,便于对照日志)
|
||||
|
||||
```
|
||||
idle → resolving → connecting → handshaking → streaming → degraded
|
||||
↑ │
|
||||
└──────── reconnecting ←───────────┘
|
||||
stopped ← 主动停止 / 1000 / 1001 / 1002(终态,不再重连)
|
||||
```
|
||||
|
||||
| 状态 | 含义 | 进入条件(发送端) |
|
||||
|--|--|--|
|
||||
| `idle` | 尚未开始连接 | 初始状态 |
|
||||
| `resolving` | 解析主机地址(DNS / 局域网发现) | `connect()` 且主机名非 IP 字面量 |
|
||||
| `connecting` | TCP + WebSocket 升级中 | 已发起请求 |
|
||||
| `handshaking` | 已连接,等待 `welcome` | `onOpen`,已发送 `hello` |
|
||||
| `streaming` | 正常推流 | 收到 `welcome` 或已成功发送首帧 |
|
||||
| `degraded` | 仍连接但异常(丢弃帧增长 / 发送失败) | `send` 失败或持续丢帧 |
|
||||
| `reconnecting` | 退避等待中 | 非主动断开 |
|
||||
| `stopped` | 已停止(终态) | 主动停止 / 1000 / 1001 / 1002 |
|
||||
|
||||
接收端(Windows)在日志与状态面板中使用同名状态描述每个客户端连接。
|
||||
|
||||
+34
-4
@@ -1,8 +1,14 @@
|
||||
project: opendance-web-server
|
||||
version: 0.7.0
|
||||
version: 0.7.1
|
||||
repository: opendance-web
|
||||
default_branch: main
|
||||
contracts_version: 0.4.0
|
||||
contracts_version:
|
||||
rest-api: 0.5.0
|
||||
websocket-keypoints: 0.2.0
|
||||
data-models: 0.7.0
|
||||
skeleton-topology: 0.2.0
|
||||
model-formats: 0.5.0
|
||||
database: 0.4.0
|
||||
|
||||
# 契约变更流程(文档 7.2):
|
||||
# 任何跨端契约变更必须先在本项目修改 contracts/ 并提升版本,
|
||||
@@ -58,15 +64,39 @@ tasks:
|
||||
file: tasks/010-deploy.md
|
||||
scope: 部署脚本(双端口:软件 API 3000 + 管理端 3001,不部署 MySQL)+ deploy/.env 初始化引导(init-env.sh,HOST 默认 0.0.0.0)
|
||||
status: done
|
||||
- id: T-WEB-011
|
||||
file: tasks/011-version-display.md
|
||||
scope: 管理端左下角展示当前版本号(唯一来源 package.json)
|
||||
status: done
|
||||
- id: T-WEB-012
|
||||
file: tasks/012-vrm-first.md
|
||||
scope: 模型主力格式全面转为 VRM(非 VRM 入库标记不可用,不进已发布目录)
|
||||
status: done
|
||||
- id: T-WEB-013
|
||||
file: tasks/013-bone-rebinding.md
|
||||
scope: 动作内部标准(33 点)+ 模型骨骼按名重绑定(含 MMD/中文名)
|
||||
status: done
|
||||
- id: T-WEB-014
|
||||
file: tasks/014-motion-preview.md
|
||||
scope: 动作管理预览(离线骨架缩略图 + 管理端 3D 回放)
|
||||
status: done
|
||||
- id: T-WEB-015
|
||||
file: tasks/015-scene-management.md
|
||||
scope: 场景管理(GLB + 重力/风/物理参数,服务端只存参数)
|
||||
status: done
|
||||
- id: T-WEB-016
|
||||
file: tasks/016-websocket-contract-020.md
|
||||
scope: 契约 websocket-keypoints 0.2.0(保活 / 设备去重 / 状态机,与 T-AND-011 对齐)
|
||||
status: done
|
||||
|
||||
deliverable:
|
||||
artifact: opendance-web-v0.7.0.tar.gz
|
||||
artifact: opendance-web-v0.7.1.tar.gz
|
||||
packaging: Linux 业务服务(系统服务托管,双端口:软件 API + 管理端,不含 MySQL、不含反向代理、不使用 Docker)
|
||||
includes:
|
||||
- web-server/ # NestJS 源码 + 构建产物(含 BVH / VMD 内置解析、ZIP 读取与模型压缩包转换)
|
||||
- admin/ # React 管理端构建产物
|
||||
- deploy/ # install.sh / init-env.sh / systemd / openrc / initd / ctl.sh / .env.example(PORT、ADMIN_PORT、HOST)
|
||||
- deploy/proxy/ # 反向代理说明(nginx / IIS,仅供参考不部署)
|
||||
- contracts/ # 当前版本契约快照(rest-api 0.5.0 / data-models 0.7.0 / database 0.4.0)
|
||||
- contracts/ # 当前版本契约快照(rest-api 0.5.0 / websocket-keypoints 0.2.0 / data-models 0.7.0 / database 0.4.0)
|
||||
- THIRD_PARTY.md # 开源声明
|
||||
- README.txt # 使用说明
|
||||
|
||||
@@ -21,7 +21,20 @@ set -euo pipefail
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
VERSION="${1:-0.7.0}"
|
||||
# 版本号唯一来源:web-server/package.json 的 version(与 GET /health 的 version 同源,
|
||||
# 也是管理端左下角展示的那个值)。不传参数时自动读取,并与 project.yaml 交叉校验。
|
||||
PKG_VERSION="$(node -p "require('./web-server/package.json').version" 2>/dev/null || true)"
|
||||
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
|
||||
VERSION="${1:-$PKG_VERSION}"
|
||||
VERSION="${VERSION:-0.7.1}"
|
||||
if [ -n "$PKG_VERSION" ] && [ "$VERSION" != "$PKG_VERSION" ]; then
|
||||
echo "[error] 版本号不一致:参数=$VERSION,web-server/package.json=$PKG_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$YAML_VERSION" ] && [ "$VERSION" != "$YAML_VERSION" ]; then
|
||||
echo "[error] 版本号不一致:参数=$VERSION,project.yaml=$YAML_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
NAME="opendance-web-v${VERSION}"
|
||||
OUT_DIR="$REPO_ROOT/dist-release"
|
||||
STAGE="$OUT_DIR/$NAME"
|
||||
@@ -169,7 +182,15 @@ if [ "$fail" -ne 0 ]; then
|
||||
die "产物完整性校验失败,已删除有问题的压缩包"
|
||||
fi
|
||||
|
||||
# ---- SHA256SUMS(裸文件名,下载目录内 sha256sum -c 直接可用) ----
|
||||
( cd "$OUT_DIR" && sha256sum "$NAME.tar.gz" > SHA256SUMS )
|
||||
log "完成:$OUT_DIR/$NAME.tar.gz"
|
||||
log "已生成 $OUT_DIR/SHA256SUMS"
|
||||
if ( cd "$OUT_DIR" && sha256sum -c SHA256SUMS --status ); then
|
||||
log "sha256 自校验:OK"
|
||||
else
|
||||
die "sha256 自校验失败"
|
||||
fi
|
||||
echo
|
||||
echo "产物内容:"
|
||||
# 先把清单读入变量,避免 tar 因管道提前关闭(SIGPIPE)而误报 "write error"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# T-WEB-011 管理端展示当前版本号
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-010
|
||||
- **文档**: v1.6 §11(版本号展示)
|
||||
|
||||
## 目标
|
||||
|
||||
管理端页面**左下角**展示当前运行版本 `vX.Y.Z`,且与 `GET /health` 的 `version`
|
||||
同源(文档 §11.1 的硬性要求:单一来源、可核对)。
|
||||
|
||||
## 方案
|
||||
|
||||
| 项 | 做法 |
|
||||
|--|--|
|
||||
| 唯一来源 | `web-server/package.json` 的 `version`(同时是 `/health` 的 `version`) |
|
||||
| 注入方式 | Vite 构建期 `define: { __APP_VERSION__: <package.json version> }` |
|
||||
| 展示位置 | `admin/src/components/Layout.tsx` 侧边栏底部 `APP_VERSION_LABEL` |
|
||||
| 禁止重复 | `admin/src/lib/version.ts` 是唯一读取点,界面不出现版本号字面量 |
|
||||
|
||||
`/health` 的 `version` 取 `process.env.npm_package_version`(由 npm 脚本注入),
|
||||
与构建期注入管理端的值来自同一个 `package.json`,因此**不可能不一致**。
|
||||
|
||||
## 验收(文档 §11.3)
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| 管理端任一页面左下角可见 `vX.Y.Z` | ✅ 侧边栏底部固定显示 |
|
||||
| 与 `GET /health` 的 `version` 相同 | ✅ 同源于 `web-server/package.json` |
|
||||
| 与产物文件名一致 | ✅ `opendance-web-vX.Y.Z.tar.gz`;打包脚本读取同一个 package.json |
|
||||
@@ -0,0 +1,32 @@
|
||||
# T-WEB-012 模型主力格式:全面转为 VRM
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-005 / T-WEB-007
|
||||
- **文档**: v1.6 §4.9
|
||||
- **契约**: `model-formats 0.5.0`、`data-models 0.7.0`
|
||||
|
||||
## 目标
|
||||
|
||||
所有入库模型统一转成 **VRM**(glTF 2.0 容器 + humanoid 扩展),
|
||||
客户端只消费 VRM,避免三种格式各自的骨骼命名/坐标系差异在客户端叠加。
|
||||
|
||||
## 方案
|
||||
|
||||
| 项 | 做法 |
|
||||
|--|--|
|
||||
| 转换默认值 | `MODEL_CONVERT_FORMAT` 默认 `vrm`(`.env.example` 同步) |
|
||||
| 直传 | `.vrm` / `.glb` 已经是 glTF 容器:直接入库、**不重编码** |
|
||||
| 需转换 | `.pmx` / `.fbx` / `.zip` 由 Assimp 转 GLB,再补齐 VRM 扩展 |
|
||||
| 可用性判定 | `isUsableModel()`:`format === 'vrm'` 才可用 |
|
||||
| 不可用模型 | **入库但标记为「不可用」**,`unusableReason()` 说明原因;不进已发布目录 |
|
||||
| 目录过滤 | `catalog/export` 只下发 `format = vrm` 的模型;`catalog/version` 指纹同样只算可用的 |
|
||||
| 默认模型 | `resolveDefaultModelId()` 只在可用模型里挑 |
|
||||
|
||||
## 验收
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| 上传 PMX/FBX/zip 后产出 VRM | ✅ Assimp → GLB → VRM 扩展补齐;实测 MMD 模型(初音未来 / 奥黛塔)转换成功 |
|
||||
| 非 VRM 模型不出现在客户端目录 | ✅ `catalog/export` 过滤 + 指纹过滤;`test/model-usability.spec.ts` 覆盖 |
|
||||
| 不可用模型有明确原因 | ✅ DTO 增 `usable` / `unusableReason` 字段 |
|
||||
| 三端契约一致 | ✅ `data-models 0.7.0` / `model-formats 0.5.0` 快照三端逐字节一致 |
|
||||
@@ -0,0 +1,42 @@
|
||||
# T-WEB-013 动作内部标准与模型骨骼重绑定
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-006 / T-WEB-012
|
||||
- **文档**: v1.6 §4.10
|
||||
- **契约**: `skeleton-topology 0.2.0`、`data-models 0.7.0`
|
||||
|
||||
## 目标
|
||||
|
||||
1. **动作内部标准**:所有动作(BVH / VMD / FBX / glTF)统一预处理成
|
||||
MediaPipe 33 点关键点序列(`mediapipe33@0.1.0`),客户端只认这一种;
|
||||
2. **模型骨骼重绑定**:任意命名风格的模型都能被关键点驱动 —— 按**名字**映射到
|
||||
canonical humanoid 骨骼,而不是要求模型符合某一种命名规范。
|
||||
|
||||
## 方案
|
||||
|
||||
### 动作内部标准
|
||||
|
||||
`preprocess.service.ts` 按扩展名分派解析器,统一产出 NDJSON 关键点序列 +
|
||||
9 个关节角特征(108 维),与 Android 端推流结构完全同构 —— 因此客户端
|
||||
DTW 的两侧输入是同一套特征。
|
||||
|
||||
### 骨骼重绑定
|
||||
|
||||
| 项 | 做法 |
|
||||
|--|--|
|
||||
| 归一化 | `normalizeBoneName()`:保留 CJK 与假名(**不能再只留 `a-z0-9`**),全角 ASCII 折叠(`足IK` → `足ik`) |
|
||||
| 候选表 | `HUMANOID_CANDIDATES` 覆盖 VRM/VRoid、Mixamo、MMD 日文名、中文常见名 |
|
||||
| 生成而非手写 | Godot 端 `src/model/humanoid_map.gd` 的 CANDIDATES **由服务端表生成**,并有跨端一致性测试 |
|
||||
| 覆盖度 | `coverage()` / `missing_bones()` 暴露给审核界面与客户端 |
|
||||
|
||||
> **踩坑记录**:第一版 `normalizeBoneName()` 只保留 `[a-z0-9]`,把 CJK 名字
|
||||
> 整个抹成空串,导致 MMD 模型 humanoid 覆盖 **0/22**。修复后实测
|
||||
> 奥黛塔 **21/22**、初音未来 **19/22**,两者 `drivable = true` 且 VRM 正常产出。
|
||||
|
||||
## 验收
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| MMD(日文骨骼名)模型可驱动 | ✅ 实测 21/22、19/22 覆盖;客户端可正常驱动 |
|
||||
| Godot 端候选表与服务端一致 | ✅ 一致性测试(跨仓库生成 + 校验) |
|
||||
| 动作统一为 33 点 | ✅ `keypoint_format = mediapipe33@0.1.0`;Android 端推流同构 |
|
||||
@@ -0,0 +1,30 @@
|
||||
# T-WEB-014 动作管理:预览功能
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-004 / T-WEB-006
|
||||
- **文档**: v1.6 §4.11(重要)
|
||||
- **契约**: `data-models 0.6.0`(新增 `thumbnailUrl`)
|
||||
|
||||
## 目标
|
||||
|
||||
审核动作时能**直接看到动作长什么样**,而不是只看文件名与帧数 ——
|
||||
动作是否可用、是否与曲目匹配,靠肉眼判断最快。
|
||||
|
||||
## 方案
|
||||
|
||||
| 项 | 做法 |
|
||||
|--|--|
|
||||
| 服务端缩略图 | `motions/motion-thumbnail.ts` 用关键点序列离线渲染骨架 PNG(前端视角的"动作剪影"),零原生依赖 |
|
||||
| 入库时机 | 预处理 `run()` 末尾生成(**非致命**:失败不阻塞预处理完成,只记录) |
|
||||
| 契约 | `data-models 0.6.0` 给 Motion 增加 `thumbnailUrl`;迁移 `1789600000000-AddMotionThumbnail` |
|
||||
| 管理端预览 | `admin/src/components/MotionPreview.tsx`:3D 骨架回放(Three.js),可拖动时间轴逐帧查看 |
|
||||
| 依赖数据 | `admin/src/lib/keypoint-sequence.ts` 解析 NDJSON,`skeleton-topology.ts` 提供父节点表 |
|
||||
|
||||
## 验收
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| 动作列表显示缩略图 | ✅ 离线渲染的骨架 PNG |
|
||||
| 可逐帧回放查看动作 | ✅ `MotionPreview` 时间轴 + 3D 骨架 |
|
||||
| 缩略图生成失败不影响预处理 | ✅ 非致命分支;错误写入日志 |
|
||||
| 契约三端一致 | ✅ `data-models 0.6.0` → 后续 0.7.0;快照逐字节一致 |
|
||||
@@ -0,0 +1,48 @@
|
||||
# T-WEB-015 场景管理(新增模块)
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-007(审核流程)
|
||||
- **文档**: v1.6 §4.12
|
||||
- **契约**: `rest-api 0.5.0`、`data-models 0.7.0`、`database 0.4.0`
|
||||
|
||||
## 目标
|
||||
|
||||
新增**场景**这一类资源:客户端跟练时的 3D 环境(GLB)+ 物理参数
|
||||
(重力 / 风 / 地面 / 碰撞体 / 环境 / 出生点 / 相机)。
|
||||
服务端**只存参数、不跑物理**,仿真在客户端(Windows 端用 Godot 内置物理)。
|
||||
|
||||
## 与模型的区别
|
||||
|
||||
| 项 | 模型 | 场景 |
|
||||
|--|--|--|
|
||||
| 主力格式 | **VRM**(humanoid 重绑定) | **GLB**(不做重绑定、不转 VRM) |
|
||||
| 转换 | PMX/FBX/zip → GLB → VRM | `.glb`/`.gltf` 直接导入;`.zip`/`.pmx`/`.fbx` → GLB |
|
||||
| 附加数据 | 骨骼 / 材质 / 缩略图 | **物理参数** `SceneConfig` |
|
||||
|
||||
## 交付物
|
||||
|
||||
| 文件 | 说明 |
|
||||
|--|--|
|
||||
| `contracts/data-models.md` §7 | `Scene` 与冻结的 `SceneConfig` 结构 |
|
||||
| `contracts/rest-api.yaml` | `/api/scenes` 增删改查 + 审核复用 `/api/reviews/scene/:id/approve\|reject` + `catalog/export.scenes[]` |
|
||||
| `migrations/1789650000000-AddScenes.ts` | `scenes` 表;`config` / `stats` 用 **MEDIUMTEXT** |
|
||||
| `src/scenes/scene-config.ts` | `normalizeSceneConfig()`:缺项默认、越界夹紧、非法枚举回退,**永不抛错** |
|
||||
| `src/scenes/{scenes.service,scene.serializer,scenes.controller,scenes.module}.ts` | 业务实现(上传一律 `pending`;删除时清理源文件/GLB/缩略图) |
|
||||
| `src/models/model-inspector.ts` | 场景复用模型检查器做 GLB 分析 |
|
||||
| `src/models/model-thumbnail.ts` | 场景复用模型光栅化器出缩略图 |
|
||||
| `admin/src/pages/Scenes.tsx` | 列表 / 上传 / 审核 / 删除 / 物理参数编辑(编辑期即按同一规则夹紧) |
|
||||
| `admin/src/lib/scene-physics.ts` | 与后端同规则的收敛(保存前就不会产生非法枚举) |
|
||||
|
||||
> **MEDIUMTEXT 的原因**:`models.stats` 曾在 TEXT(65,535 字节)上溢出
|
||||
> (实测 70,980 字节 → MySQL 1406 → 500)。场景的 `stats` 与 `config` 同样可能很大,
|
||||
> 因此**一开始就用 MEDIUMTEXT**,并保留 `shrinkStatsForStorage()` 兜底。
|
||||
|
||||
## 验收
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| 场景可上传(GLB 直传 / zip 自动解包 / PMX 转换) | ✅ 与模型同一套转换管线 |
|
||||
| 物理参数可在管理端可视化编辑 | ✅ 重力 / 风 / 摩擦 / 弹性 / 碰撞体 / 环境 / 出生点 / 相机 |
|
||||
| 非法参数不会入库 | ✅ 前后端同规则收敛;`test/scene-config.spec.ts` 9 项边界用例 |
|
||||
| 客户端能拿到场景 | ✅ `catalog/export.scenes[]`;目录指纹纳入场景变更 |
|
||||
| 路由与契约一致 | ✅ `check-routes` 共 **35** 条 |
|
||||
@@ -0,0 +1,48 @@
|
||||
# T-WEB-016 契约提升:websocket-keypoints 0.2.0(保活 / 去重 / 状态机)
|
||||
|
||||
- **状态**: done
|
||||
- **依赖**: T-WEB-003(本项目为契约源)
|
||||
- **文档**: v1.6 §6.5 / §7.2
|
||||
- **触发**: Android 端 T-AND-011「连接状态异常、频繁重连」要求**两端一起改**
|
||||
|
||||
## 背景
|
||||
|
||||
按文档 §7.2,任何跨端契约变更必须**先在本项目修改 `contracts/` 并提升版本**,
|
||||
再向另两端建迁移任务。本次 Android 端排查出"频繁重连"的根因在协议层:
|
||||
接收端有静默超时,但**契约里没有声明超时值,也没有定义任何应用层保活消息**。
|
||||
|
||||
## 变更内容(均为**向后兼容**:子协议名与二进制帧 `version` 均不变)
|
||||
|
||||
| 章节 | 新增 | 为什么必须写进契约 |
|
||||
|--|--|--|
|
||||
| §1.1 保活 | 接收端必须在 `welcome`(及需要变更时的 `config`)中声明 `silentTimeoutMs` 与 `heartbeatIntervalMs`;且**心跳必须小于静默超时的一半** | 客户端原先只能用 OkHttp 的**协议层** ping,而它由底层库自动应答、**不暴露给应用层**,接收端根本看不到,无法续命 |
|
||||
| §3.1 | `hello.deviceId`(该安装实例的稳定标识,**不使用任何硬件序列号**) | 没有它,一次闪断重连会被接收端显示成两台设备 |
|
||||
| §3.2 / §3.3 | `welcome` / `config` 携带上述保活字段 | 让客户端按服务端节奏保活,而不是各写一套魔数 |
|
||||
| §3.6 | 应用层 `ping` / `pong`(回显 `sentAtMs`,`pong` 另带 `serverAtMs` 供算 RTT) | 协议层 ping 对应用层不可见,必须自定义 |
|
||||
| §5 | 设备去重规则:同一 `deviceId` 只保留最新连接,旧连接以 `1000` 关闭 | 用 `1000` 是刻意的:对端据 §7 不会重连,否则会形成"挤掉-重连-再挤掉"的循环 |
|
||||
| §7 | 关闭码与重连策略:`1000` / `1001` / `1002` 与主动停止**不重连**;其余重连 | 原实现"只要能重连就重连",服务端重启或协议不匹配时也会疯狂重连 |
|
||||
| §7.1 | 退避参数冻结:`min(500ms * 2^(n-1), 15000ms)` + **±20% 抖动** | 避免同网段多台设备同时重连打尖峰 |
|
||||
| §8 | 连接状态机 `idle → resolving → connecting → handshaking → streaming → degraded → reconnecting → stopped` | 两端同名,日志可直接对照 |
|
||||
|
||||
## 兼容性
|
||||
|
||||
* 子协议 `opendance.pose.v1` 与二进制帧 `version = 0x01` **均不变**;
|
||||
* 只**新增可选**控制字段与一种新控制类型;
|
||||
* 仅实现 v0.1.0 的一端仍可正常互通(它不发 `ping`、不读 `silentTimeoutMs`,
|
||||
但每帧关键点 + 每秒 `stats` 已足够保活)。
|
||||
|
||||
## 改动清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|--|--|
|
||||
| `contracts/websocket-keypoints.md` | 上文全部章节 |
|
||||
| `contracts/VERSION` | `websocket-keypoints: 0.1.0` → `0.2.0` |
|
||||
| `web-server/src/config/contract-versions.ts` | 编译期常量同步为 0.2.0(`check-contracts.mjs` 强制) |
|
||||
|
||||
## 验收
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| `npm run contracts:check` 通过 | ✅ 六项契约版本一致,35 条路由与契约一致 |
|
||||
| 三端快照逐字节一致 | ✅ windows / android 的 `contracts/` md5 与 web 完全相同 |
|
||||
| 另两端的迁移任务已建立 | ✅ Windows `T-WIN-015`、Android `T-AND-011` 均已落地并引用本契约 |
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "opendance-admin",
|
||||
"private": true,
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"type": "module",
|
||||
"description": "OpenDance Studio 管理端(React + TypeScript + Vite)",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opendance-web-server",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "OpenDance Studio Web 管理服务端(NestJS + TypeORM + 现有 MySQL 实例)",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
||||
*/
|
||||
export const CONTRACT_VERSIONS: Readonly<Record<string, string>> = Object.freeze({
|
||||
'rest-api': '0.5.0',
|
||||
'websocket-keypoints': '0.1.0',
|
||||
'websocket-keypoints': '0.2.0',
|
||||
'data-models': '0.7.0',
|
||||
'skeleton-topology': '0.2.0',
|
||||
'model-formats': '0.5.0',
|
||||
|
||||
Reference in New Issue
Block a user