20 Commits
Author SHA1 Message Date
zijunchiang 3738bd91b9 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。
2026-09-17 01:30:41 +08:00
zijunchiang 50073b97cf docs(contracts): 补齐 rest-api 里 CatalogBundle.scenes[] 与 Scene/SceneConfig 结构
`catalog/export` 自 v0.7.0 起就返回 `scenes[]`(catalog.service.ts 已实现,
data-models.md §7 也已冻结),但 rest-api.yaml 的 CatalogBundle 漏了这个字段,
且 components 里没有 Scene / SceneConfig 定义 —— 属于"实现已发布、规格漏写"。

本次只补规格,不改线格式,因此六个契约版本号均不变(rest-api 仍为 0.5.0,
与文档 v1.6 §5.6「客户端契约升级到 rest-api 0.5.0」一致):
- CatalogBundle 增加 scenes[](标注 0.5.0 起下发)
- 新增 Scene(含 glbUrl/thumbnailUrl/config/stats/来源五元组/status)
- 新增 SceneConfig(physics.gravity/wind/ground/colliders、environment、
  spawnPoints<=16、camera)与复用的 Vec3

注入依赖:Windows 端 T-WIN-013 需要按这份结构生成 GDScript 类型,
规格缺失会导致两端对字段边界(如 0~1、±200)的理解不一致。

同时补 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(交付缺陷)。
2026-09-17 00:46:35 +08:00
opendance 584b7eaca9 feat(scenes): 场景管理后端 + 契约 0.5.0/0.7.0/0.4.0(T-WEB-015)
契约先行(三端逐字节一致,已同步 windows/android):
- rest-api -> 0.5.0:新增 GET/POST /api/scenes、GET/PATCH/DELETE /api/scenes/{id}
- data-models -> 0.7.0:新增 §7 Scene 与 7.1 SceneConfig(物理/环境/出生点/相机)
- database -> 0.4.0:新增 scenes 表(config/stats 用 MEDIUMTEXT,迁移可逆)

后端:
- entities/scene.entity.ts + 迁移 AddScenes1789650000000(scenes 表)
- scenes/scene-config.ts:SceneConfig 定义 + 默认值 + **边界收敛**(越界夹紧、
  非法枚举回退),保证客户端拿到的参数一定可解释;服务端只存参数不跑物理
- scenes/scenes.service.ts:上传(.glb/.gltf 直接导入;.zip/.pmx/.fbx 转 GLB,
  场景不做 humanoid 重绑定、不转 VRM)、列表/详情/更新/软删除、缩略图复用模型
  光栅化器、分析复用模型检查器、config 同时接受 JSON 字符串与平铺字段
- scenes/scenes.controller.ts + scenes.module.ts(app.module 注册)
- 审核复用 /api/reviews/:type/:id:TargetType.Scene 接入待审列表与状态流转
- catalog/export 增 scenes[],目录指纹纳入场景变更
- 枚举:TargetType.Scene、AssetKind.Scene / SceneThumbnail

测试:新增 test/scene-config.spec.ts 6 项(默认值/夹紧/枚举回退/出生点上限/颜色/
序列化);规模 19 文件 / 243 项;check-routes 35 条一致。版本 0.6.0。

注:管理端场景页(GLB 预览 + 物理参数可视化)与 Windows 场景选择留待下一轮。
2026-09-17 00:27:08 +08:00
opendance aad60cbd2e feat(motions): 动作预览——骨架缩略图 + 管理端 3D 回放(契约 data-models 0.6.0)
按《DSH 开发文档 v1.6》4.11(T-WEB-014,用户标注「重要」):

服务端:
- 抽出公共 PNG 编码器 src/common/png.ts(模型缩略图改为复用,零原生依赖不变)
- 新增 src/motions/motion-thumbnail.ts:关键点序列 -> 正视图骨架 PNG
  (复用 33 点 PARENT_TABLE 连线,包围盒按整段计算,低置信度点不画)
- motions 增 thumbnail_asset_id(迁移 AddMotionThumbnail)+ AssetKind.MotionThumbnail
- /preprocess 生成缩略图并落库,失败只记 warning;MotionDto 增 thumbnailUrl(契约 0.6.0)

管理端:
- 新增 MotionPreview:懒加载 NDJSON -> 骨架线框 + 关节实例球体
  支持播放/暂停、上一帧/下一帧、时间轴拖动、0.25x-2x 变速、循环
  叠加帧号/当前时间/总时长,按整段包围盒自动取景
  three.js 单独分包(lazy + Suspense),主包 241 kB 不涨
- 动作列表新增 40px 骨架缩略图列(点击开预览)+ 预览按钮 + 内联预览面板
- 新增 admin/src/lib/keypoint-sequence.ts 与 lib/skeleton-topology.ts

测试:新增 8 项;规模 18 文件 / 237 项。版本 0.5.0。
2026-09-17 00:21:09 +08:00
opendance 9eae6f3e43 feat(models): 骨骼重绑定——修复 MMD 模型 humanoid 恒为 0/22(契约 skeleton-topology 0.2.0)
根因:normalizeBoneName() 只保留 a-z0-9,**中日文字符被整个丢掉**:
'上半身'/'左ひじ'/'首' 归一化后都是空串,MMD 模型的骨名必然失配,
humanoid 覆盖率恒为 0/22 -> 不可驱动 -> 只能回退交付 GLB 并标记不可用。

修复:
1. 归一化保留 CJK/平假名/片假名/长音符/々,并把全角 ASCII(足IK)折成半角;
2. 候选表补齐四类命名(22 根 x 8-13 个候选):VRoid / Mixamo /
   MMD 标准骨架(下半身/上半身/上半身2/首/頭/左肩/左腕/左ひじ/左手首/
   左足/左ひざ/左足首/左つま先)/ 常见中文名(臀部/头/脖子/左大臂/左大腿…);
   精确匹配保证「左足IK」不会抢占「左足」;
3. Windows 客户端 humanoid_map.gd 的 CANDIDATES 由服务端表**生成写入**,
   normalize() 同步改造,parity 测试断言键/候选顺序完全一致;
4. 契约 skeleton-topology -> 0.2.0:新增 §6「内部动作标准与模型骨骼重绑定」
   (三层标准、22/16 根骨、归一化与匹配规则、实测记录);
5. 新增 4 项测试;规模 17 文件 / 229 项。

实测:奥黛塔 PMX 0/22 -> 21/22、初音未来 PMX 0/22 -> 19/22,均 drivable=true 且产出 VRM。
版本 0.4.0。
2026-09-17 00:10:07 +08:00
opendance 39bfd390e3 feat(models): 模型一律转 VRM(契约 0.5.0)+ 可用性门槛 + 管理端版本号展示
按《DSH 开发文档 v1.6》4.9 与第十一章实现,Web 端第一步:

1. 契约先行(先改 contracts/ 再同步三端):
   * model-formats.md -> 0.5.0:模型主力格式改为 VRM,所有上传模型一律转 VRM
     (.pmx/.fbx: assimp -> GLB -> VRM;glb/gltf: 包装为 VRM);
     新增 humanoid 硬门槛与"不达标回退 GLB + usable=false"的交付表;
   * data-models.md -> 0.5.0:Model 新增 usable / unusableReason,明确
     usable=false 不进入 catalog/export;format 说明改为主力 vrm;
   * VERSION 与 contract-versions.ts 同步(三端契约文件逐字节一致)。
2. 实现:
   * MODEL_CONVERT_FORMAT 默认改为 vrm(deploy/.env.example 同步);
   * 新增 isUsableModel()/unusableReason():usable 等价于"已产出 VRM",
     原因从 stats 里带出 humanoid 覆盖度;
   * catalog/export 与 catalog/version 指纹都只统计 format=vrm 的已发布模型,
     resolveDefaultModelId 同样只在可用模型中选;
   * 管理端:模型列表与审核面板显示「不可用」及原因;
   * 管理端左下角展示当前版本号(构建期从 web-server/package.json 注入
     __APP_VERSION__,与 GET /health 同源),并去掉写死的"契约 rest-api 0.1.0"。
3. 测试:新增 test/model-usability.spec.ts 5 项;规模 16 文件 / 225 项。
4. 版本 0.3.0。
2026-09-17 00:05:02 +08:00
opendance 60c182f1b2 docs: 消除 Gitea 模棱两可 Unicode 告警(全角标点紧邻 ASCII 时改为半角)
依据 Gitea v1.27.3 modules/charset/escape_stream.go 的判定规则
(analyzeDetectResults:仅当左右相邻字符都不是非 ASCII 时才标记,
且 _common 表恒定生效),把说明文档中会被标记的全角标点
(,:;?!() 等)改为半角,并保证成对括号宽度一致。
中文语境中的全角标点(左右均为中文)保持原样,不动代码与契约语义。

同时删除临时探针目录 _probe/(仅用于确认 Gitea 判定规则)。
2026-09-15 21:45:51 +08:00
zijunchiang 6913e181b5 docs: 其余易混淆字符改 ASCII(× − ≥ ≤ ≈ ✓ 等),消除 Gitea 告警
实测确认 Gitea 的「模棱两可 Unicode」检测覆盖:— – … § → × − ≥ ≤ ≈ ✓。
三库 .md/.txt 全部替换为 x - >= <= ~ OK;contracts 三库同源保持一致。
重新打包 v0.2.0。
2026-09-15 21:35:10 +08:00
zijunchiang 5de0891706 docs: 修回被误压空格的 ':=' 写法;契约与文档去真实 MariaDB 版本串
- contracts/websocket-keypoints.md(三库同源):把上一轮空格清理误伤的
  'var x:= ...' 修回 'var x := ...',保持文档内代码样例可读
- contracts/rest-api.yaml(三库同源):'如 MariaDB 10.11.19' -> '如 MariaDB 10.5+'
- DSH.md / tasks/001:去掉真实部署实例版本号,改为'由部署方确认'的表述
- 重新打包 v0.2.0

验证:契约 6 项一致(三库 md5 相同)、30 条路由一致。
2026-09-15 21:33:20 +08:00
zijunchiang 8c2f2afaea docs: 三库文档规范化(去混淆字符 + 占位符)+ Web README 初次部署清单
- 三库所有 .md/.txt 与配置样例中的 — – … § → 一律换成 ASCII(Gitea 的
  「模棱两可的 Unicode 字符」告警即由这几个字符触发;中文全角标点不受影响,
  实测 README.txt 修正后告警消失)
- contracts/* 在三库保持逐字节一致(已校验 md5)
- 残留真实值清理:README.txt 建库 SQL 的 dpendance -> opendance/opendance_app,
  Android HostDiscovery.kt 注释示例 192.168.1.23 -> 192.0.2.23
- Web README.txt 新增「初次部署(从零跑起来)」8 步清单与「常见问题(初次部署)」
  7 条;去掉纯文本 README 里的 markdown 粗体标记;统一 '以 root 直接执行' 写法
- 重新打包 v0.2.0

验证:168 项单测通过;契约 6 项与 30 条路由一致。
2026-09-15 21:28:12 +08:00
zijunchiang ebd871018f docs(readme): 新增初次部署清单与常见问题;统一 root 执行写法;清除 markdown 粗体标记与残留真实库名 2026-09-15 21:27:29 +08:00
zijunchiang 9f08d316d0 docs: 清除文档中的真实部署信息与易混淆 Unicode 字符
需求:公开仓库的说明文档不应暴露开发/部署期信息,且 Gitea 会对 README.txt
报"含模棱两可的 Unicode 字符"。

敏感信息(全部替换为占位符或文档示例值):
- 真实数据库实例:DB_HOST=192.168.150.10 / DB_USER=dpendance / DB_NAME=dpendance
  → <数据库主机> / opendance_app / opendance(web-server/.env.example、
  deploy/.env.example、contracts/database.md ×3 库、project.yaml、README.txt
  的建库 SQL、tasks/001、tasks/002)
- 内网网段 192.168.150.0/24 / 192.168.150.20 / 192.168.1.10
  → RFC 5737 文档网段 192.0.2.0/24 等(含测试夹具,行为不变)
- 真实 Gitea 域名 → https://<Gitea 地址>/<owner>/<repo>(systemd 单元、
  Windows/Android README 的交叉链接)
- 测试夹具里的真实口令字面量 → 虚构口令(仍覆盖特殊字符场景)
- 保留 /opt/opendance、/var/lib/opendance 等产品默认安装路径(非敏感)

README.txt 易混淆字符:
- 全角拉丁字母(I/K 等)与全角 ASCII 标点((),:;?)→ 半角 + 中文排版空格
- 正文里的乱码字节样例(模å…)→ 改为文字描述
- × → x,★ → *
- 注意:代码/测试/契约里的全角「IK」是 MMD 真实骨骼名,**必须保留**,
  已回滚误改(mmd-skeleton.ts、mmd-vmd.spec.ts、inspect-vmd.mjs、各契约)

验证:168 项单测通过(含 VMD 31 项,确认全角骨骼名未被破坏)。
2026-09-15 21:25:25 +08:00
zijunchiang c342a804a1 feat(models): 新增历史模型回填(reanalyze)并修复存储去重的悬挂文件问题
背景:升级到带 models.stats 的版本后,库里已有模型没有分析与缩略图,
审核界面会全部显示"未分析、无预览图"。E2E 验证时还发现一个真问题:
sha256 去重命中旧资产但文件不存在(换过 STORAGE_DIR、或只恢复数据库)时,
分析会静默产出"全 0 结果"。

- POST /api/models/:id/reanalyze:重新分析(面数/骨骼树/humanoid/可驱动性)
  并重建缩略图,只改 stats 与 thumbnailAssetId;文件在存储中不存在时返回 409,
  避免用空结果覆盖已有分析数据
- scripts/reanalyze-models.mjs:批量回填 CLI(--dry-run / --model-id / --force)
- StorageService.persistBuffer:去重命中但文件不存在时按原路径**自愈补写**
- 契约:rest-api 新增 /api/models/{id}/reanalyze(路由 29 → 30 条)
- 文档:README / tasks/005 / 发布说明补回填章节与升级步骤

实测:真实 21MB VRM(290 骨骼)回填出面数 53552 / 顶点 44662 / humanoid 7/22,
并重建 512×512 缩略图;文件缺失场景返回 409 且不覆盖既有数据。
测试 168 项通过;契约与 30 条路由一致。
2026-09-15 21:18:15 +08:00
zijunchiang 53bf182476 feat(models): 发布 v0.2.0 —— GLB/VRM 主力格式、自动缩略图与分析、模型审核与骨骼预览
需求 1:系统确定以 GLB / VRM 为模型主力格式
- 服务端新增支持库:
  - model-inspector.ts:解析 GLB/VRM 的面数、顶点、材质、贴图、形变目标、
    **骨骼树(名称/层级/是否蒙皮/humanoid 映射/子节点数)**、humanoid 覆盖率与可驱动性
  - model-thumbnail.ts:**纯 TS 软件光栅化**(z-buffer + Lambert + 完整蒙皮矩阵)
    生成 512×512 PNG 缩略图,零原生依赖、不装 headless-gl
  - vrm-writer.ts:GLB → VRM 1.0(写入 VRMC_vrm 扩展与 humanoid 映射)
  - gltf-packer.ts:glTF(.gltf + .bin + 贴图)→ 单文件 GLB
- humanoid 候选表与 Godot 客户端 humanoid_map.gd **逐字对齐**,并有单测断言两边一致
  (服务端判定的"可驱动" = 客户端实际能否驱动)

需求 2:模型管理
- .glb / .vrm / .gltf **直接导入**(不重编码,GLB 含 VRM 扩展会自动识别为 VRM)
- .pmx / .fbx 服务端转换:assimp → .glb →(失败)→ .gltf 后自行打包成 GLB
  →(MODEL_CONVERT_FORMAT=vrm 时)补 VRMC_vrm 产出 VRM 1.0;每步 assimp info 回读校验
- .zip **自动搜索包内可用模型**:.vrm → .glb → .gltf(直接导入)优先,
  其次 .pmx → .fbx(转换);同优先级内按"指名字 → 目录最浅 → 文件最大"
- 入库后自动分析(models.stats)与生成缩略图(models.thumbnail_asset_id),
  失败均不阻断入库;管理端列表显示小缩略图、预览放开到 vrm/glb/gltf

需求 3:审核功能
- 上传的模型状态**一律 pending**(不再自动发布、不再自动设默认)
- 审核界面新增模型面板:体积、面数、顶点、材质、贴图、骨骼、形变目标等指标,
  humanoid 覆盖进度、可驱动性结论(缺哪些必需骨骼)、warnings,以及
  **骨骼详细预览**(层级缩进、蒙皮/Humanoid 徽章、子节点数、搜索保留祖先链、
  仅看蒙皮、限高滚动 + 只渲染前 400 行)
- 审核通过可带 setPrimary=true 一并设为默认模型(此时才写源码种子);
  POST /api/models/:id/primary 仅允许 published(否则 409)
- scripts/import-default-model.mjs 改为"上传 → 审核通过 + 设默认"流程

其它:
- models 新增 stats 列(迁移 AddModelStats1789460000000,已在真实库执行)
- 契约升至 rest-api/data-models/model-formats 0.4.0、database 0.3.0(另两库已同步)
- 版本号 0.1.0 → 0.2.0(package.json ×2 + lockfile + project.yaml)
- 文档:README.txt / DSH.md / project.yaml / tasks 005·007·010 全面更新;
  顶层开发书更新为 v1.4

验证:168 项单测通过(10 文件);契约 6 项与 29 条路由一致;typecheck/build 通过;
真实端到端:MMD 压缩包 → GLB + 分析(面数 9071/骨骼 238/humanoid 0-22) + 缩略图(15KB PNG);
GLB 直接导入 sha256 与源文件一致;审核通过后 defaultModelId 由 VRM 变为该模型
2026-09-15 20:37:53 +08:00
zijunchiang ccced34dd6 fix(models): 默认模型改用「压缩包 sha256」匹配(转换产物非字节级确定)
实测发现:同一份 zip 连续转换三次得到三个不同的产物 sha256(大小都是 1,152,464
字节,差异集中在 BIN chunk 的十几字节),说明 Assimp 的 GLB 导出不是字节级确定的。
因此原先把「转换产物 sha256」当作默认模型匹配键的设计是错的——换一次转换就失效。

- default-model.ts / default-model-source.ts:种子拆成 sourceSha256(上传压缩包的
  sha256,稳定,主匹配键)与 fileSha256(产物 sha256,仅作记录)
- models.service.ts:resolveDefaultModelId 依次按 压缩包 sha256 → 产物 sha256 →
  名称 匹配,都落空才回落到 VRM 优先规则并告警
- ModelDto 增加 sourceSha256(非压缩包上传为 null)
- scripts/import-default-model.mjs:写种子时用 sourceSha256;顺带修掉位置参数解析
  (文件当第一个参数时识别不出,会打印用法)
- 契约与文档同步:data-models §4.1、model-formats §1.1 记录该确定性结论;
  README / DSH / tasks/005 的「按产物 sha256 匹配」表述已更正

另附:把模型格式能力的实测矩阵写进 Release 说明(原生只支持 glTF 系;
PMX→FBX 失败;VRM→GLB 丢 VRM 扩展;humanoid 骨骼命名覆盖率)。

验证:125 项单测通过;清理掉两个"文件只在本机临时目录"的模型(避免 sha256 去重
导致后续上传不落盘);种子按压缩包 sha256 命中后 defaultModelId 正确解析。
2026-09-15 19:36:37 +08:00
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
zijunchiang bba5bcf59e feat(web): 端口分离 + VMD 动作支持;契约升至 0.2.0
需求 1:管理端端口与软件 API 端口分离
- 软件 API 端口 PORT(默认 3000)只提供 /api、/health、/static、/contracts,
  不提供管理页面(未命中路径 404);管理端端口 ADMIN_PORT(默认 3001)
  提供管理页面与全部接口;ADMIN_HOST 可单独限制管理端来源
- main.ts 同进程监听两个端口,用 req.socket.localPort 区分端口行为;
  ADMIN_PORT=PORT 为单端口兼容模式(只创建一个 server,无 EADDRINUSE)
- 端口非法在生产环境中止启动;ADMIN_PORT=PORT 只 warn
- 启动日志分别打印两个端口的访问地址

需求 2:动作数据支持 VMD(MikuMikuDance)
- vmd-parser.ts:二进制解析(30 字节头 + Shift-JIS 模型名 + 111 字节/条骨骼
  关键帧 + morph 段)、MMD 贝塞尔插值、GBK 模型名回退、固定 30fps
- mmd-skeleton.ts:标准 MMD 骨架;静止比例从参考 PMX 骨骼段实测得到
  (足首 1.43 / 膝 6.57 / 髋 11.50 / 首 16.21 / 頭 17.16,腿长 10.09)
- mmd-pose.ts:FK + 两骨骼解析腿部 IK(膝盖朝正面,不可达收敛为伸直)
  + 左手系→右手系 Z 镜像(由真实数据对照实验确定:94.1% vs 5.9%)
- retarget.ts:抽出 worldFramesToKeypointFrames(),BVH 与 VMD 共用重定向
- MotionFormat 增 vmd、扩展名映射增 .vmd;管理端上传支持 .vmd
- scripts/inspect-vmd.mjs:上传前自查工具

契约:rest-api / data-models / model-formats → 0.2.0(另两库已同步逐字节一致)

验证:85 项单测通过(VMD 31 项);契约与 29 条路由一致;typecheck/build 通过;
真实样本经 HTTP 端到端:霜雪千年 2901 帧(IK 腿)、恋爱循环 4604 帧(FK 腿)
均 preprocessState=done、0 NaN、0 越界(测试数据已从数据库清理)。
2026-09-15 08:47:37 +08:00
zijunchiang 321ab1f584 docs(web): 清理 Docker/nginx 残留引用,反代职责边界写进任务书与契约
- tasks/010-deploy.md: 按"仅业务服务、不部署反代、服务自托管管理端"重写,
  补充四层进程降级、无 sudo 降权、上传上限两侧一致等实现说明与验收表
- deploy/uninstall.sh: 删除 nginx 站点清理逻辑(本项目从未安装过反代,
  也不应改动运维侧的 nginx / IIS 配置)
- scripts/package-release.sh: 修正产物结构注释,去掉已删除的
  web-server/admin/nginx.conf 拷贝与 deploy/nginx 备份清理
- web-server/.env.example: 修正"容器内路径 / docker-compose 挂载"过时注释
- contracts/rest-api.yaml: 服务地址描述改为业务服务部署语义
2026-09-15 01:27:47 +08:00
zijunchiang 75498db1c0 feat(web): 部署方式由 Docker 改为 Linux 原生站点(nginx + systemd)
需求变更:Web 服务端不再使用 Docker,改为直接运行在 Linux 上的网站。

- 移除 deploy/docker-compose.yml、web-server/Dockerfile、
  web-server/admin/Dockerfile、web-server/.dockerignore
- 新增 deploy/install.sh:建服务用户与目录 → 装运行期依赖 → 迁移
  → 注册 systemd 服务 → 配置 nginx 站点 → 健康检查,一键完成
- 新增 deploy/systemd/opendance-api.service:ProtectSystem=strict 等
  安全加固,ReadWritePaths 限定为上传目录
- 新增 deploy/nginx/opendance.conf:同站点托管管理端静态资源 +
  反代 /api /health /static,client_max_body_size 512m,SPA 回退
- 新增 deploy/uninstall.sh:默认保留数据,--purge 才删除
- 新增配置项 HOST(AppConfig.bindHost):原生部署下 Node 只监听
  127.0.0.1,不直接对外暴露,实测外部 IP 不可达
- 同步更新 contracts/rest-api.yaml、DSH.md、project.yaml、README.txt、
  tasks/008、tasks/010 与发布打包脚本
2026-09-14 22:42:56 +08:00
zijunchiang 9b8fc6be96 chore(contracts): 冻结 v0.1.0 跨端契约(REST/WebSocket/数据模型/拓扑/格式/数据库) 2026-09-14 19:00:55 +08:00