正常路径之外的**字段边界**同样只在跨端跑起来才暴露。配合 Web v0.7.5 增加:
1. 不可用模型:用 Assimp 把真实 PMX 转成 GLB 后入库(等价"管线只产出 GLB"),
Web 侧断言 usable=false、原因准确(骨架达标时不得说"humanoid 不达标")、
不在 catalog/export、defaultModelId 不变;Windows 侧(包内探针)断言
bundle.models 里没有任何非 VRM 模型、按 id 找不到该模型、defaultModelId 仍指向可用 VRM。
2. 场景 config 只给 {physics:{gravity:{y:-9.8}}}:Web 侧断言服务端补齐默认
(碰撞体 auto、地面 0.6/45°、静风、相机 follow、环境光 1、出生点 1 条);
Windows 侧断言客户端解析出的**同一组默认值**逐项一致。
实测(Web v0.7.5 × Windows v0.2.5 包内 pck,真实 MySQL):
✓ usable=false 且原因为「未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标)…」
✓ 目录里的模型全是 VRM/不可用模型 #2 不在目录里/defaultModelId 仍为 1
✓ 最简配置:重力 -9.8 保留,碰撞体 auto、地面 0.6/45°、静风、相机 follow、
环境光 1.0、出生点 1 条,两端逐项一致
跨端集成验收全部通过。
顺带修掉播种脚本自身的顺序错误(把依赖 catalog 的断言插到了 catalog 取回之前),
以及把断言改成「先取目录再断言」。不影响发布产物(build/ 已排除出 pck)。
7.4 KiB
T-WIN-019:跨端集成验收(包内客户端 × 真实 Web 服务端)
- 状态: done
- 依赖: T-WIN-013(场景选择)/ T-WIN-018(发布包级验收)/ T-WEB-015(场景管理)
- 文档: v1.6 §5.6 / §7.5
- 契约:
rest-api 0.5.0、data-models 0.7.0、skeleton-topology 0.2.0
为什么需要
本仓的单测用的是假 HTTP 夹具(tests/test_api_integration.gd 打本地小服务),
Web 端的测试只验自己那一侧。两端各自绿灯 ≠ 契约字段真的对得上:
字段名、嵌套层级、null 与缺字段、单位(米/度/秒)、归一化范围,
都可能在一侧对、另一侧错。契约 0.5.0 新增的 scenes[] 与物理参数尤其如此。
所以本任务是:拿发布包里的客户端(包内 pck)打真实服务端。
发现并修复的缺陷
探针首次运行就抓到一个真实缺口:
服务端 GET /api/catalog/export 的 motion 条目(契约 data-models 0.6.0 起):
...,"keypointUrl":"...","thumbnailUrl":"/static/motion-thumbnail/.../x.png","keypointFrames":2901,...
客户端的 CatalogTypes.Motion:
✗ 没有 thumbnail_url 字段(服务端一直在发,客户端默默丢掉)
而同一文件的 Model / Scene 都有 thumbnail_url。
修复:src/net/catalog_types.gd 的 Motion 增加 thumbnail_url 并解析
thumbnailUrl(缺失时为 "",兼容旧服务端);新增
tests/test_catalog_types.gd(4 项 / 20 条断言)用服务端真实报文形状锁住
「三个资源类型都消费 thumbnailUrl」,并显式记录一处刻意不消费的差异:
Model.usable / unusableReason —— 它们是 Web 端审核用的,
而 catalog/export 只下发可用模型(目录里恒为 true),
不可用模型的可见性由 Web 管理端负责。
实现
| 文件 | 作用 |
|---|---|
build/integration/probe-integration.gd |
跨端集成探针:只加载包内代码(--main-pack + res://),打真实服务端并逐项断言 |
build/integration/seed-catalog.mjs |
通过真实 Web API 播种一份真实目录(真 VMD → 内部标准;真 PMX → VRM;场景 GLB + 物理参数),并写出期望值 JSON |
build/accept-integration.sh |
编排:检查服务端可达 → 播种(可跳过)→ 对包内 pck 跑探针 → 汇报 |
探针断言(对应需求编号)
| 需求 | 断言 |
|---|---|
| ⑤ API 升级 | /health 连通 + 六个契约版本与 Web 一致;/api/catalog/version 指纹非空;catalog/export 四类资源齐备;defaultModelId 解析为默认模型 id |
| ⑤ 场景选择 + 物理参数落地 | 逐项比对类型化后的场景参数:重力 y、基础风 x/z、阵风幅度/频率、风坐标系、地面摩擦/坡度、碰撞体模式、出生点 yaw、相机 mode/distance/height、环境光与雾;另外校验 Wind.sample(0) 等于基础风;场景 GLB 可下载且魔数与 fileSize 一致 |
| ② 动作内部标准 | keypointFormat == mediapipe33@0.1.0、服务端帧数、is_scorable();拉取 NDJSON 后客户端解析帧数 == 服务端帧数、skipped_lines == 0、首帧 33 点、每帧带服务端关节角、时间轴推导帧率 ≈ 30 fps、时长 ≈ 96.7 s |
| ③ 动作预览(资产链路) | 动作缩略图 可下载且为 PNG |
| ① 模型交付 | 模型格式 vrm、isPrimary、模型文件可下载且为 glTF 容器、大小与 fileSize 一致 |
契约边界场景(续,配合 Web v0.7.5)
正常路径之外的字段边界同样只在跨端跑起来才会暴露。seed-catalog.mjs 与
probe-integration.gd 因此增加两类场景,两端都断言:
| 场景 | Web 侧(播种脚本断言) | Windows 侧(包内探针断言) |
|---|---|---|
| 不可用模型:真实 PMX 用 Assimp 转 GLB 后入库(等价"管线只产出 GLB") | usable=false;原因准确(骨架达标时不得说"humanoid 不达标");不在 catalog/export;defaultModelId 不变 |
bundle.models 里没有任何非 VRM 模型;按 id 找不到该模型;defaultModelId 仍指向可用的 VRM |
场景 config 只给最少字段({physics:{gravity:{y:-9.8}}}) |
服务端补齐默认(碰撞体 auto、地面 0.6/45°、静风、相机 follow、环境光 1、出生点 1 条) |
客户端解析出的同一组默认值逐项一致 |
实测(Web v0.7.5 × Windows v0.2.5 包内 pck):
✓ 该模型标记为不可用(usable=false)
✓ 不可用原因说明"未产出 VRM":未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标),需重新以 VRM 交付才能进入客户端目录
✓ 骨架达标(drivable=true,humanoid 19/22)时不得归因于 humanoid
✓ 目录里的模型全是 VRM(不可用模型不进目录)/✓ 不可用模型 #2 不在客户端目录里
✓ defaultModelId 仍是可用的 VRM 模型(实际 1,期望 1)
✓ 最简配置:重力保留 -9.8 / 碰撞体补默认 auto / 地面默认 0.60 / 45°
✓ 最简配置:风默认静风(is_calm=true)/ 相机默认 follow / 环境光默认 1.0 / 出生点 1 条
这两类场景的价值在同一份契约的两侧解释:Web 认为"不可用"的模型长什么样、 客户端是否真的看不到它;以及"缺字段"时两端的默认值是否逐项一致 (服务端补齐 vs 客户端收敛,任一侧不一致都会表现为"配置看起来生效了但行为不对")。
实测(v0.2.3 包内 pck × 真实 Web v0.7.4 + 真实 MySQL)
[integration] Windows 包内客户端 × 真实 Web 服务端:http://127.0.0.1:3100
✓ GET /health 200(服务端版本 0.7.4);契约 6 项全部一致
✓ 目录指纹非空;目录内嵌契约版本与 /health 一致
✓ 目录四类资源齐备:songs=1 motions=1 models=1 scenes=1
✓ defaultModelId 解析为 1
✓ 场景 glbUrl/fileSize;重力 y=-3.50;基础风 (2.0,0.0,-1.0);阵风 4.0 m/s @ 0.50 Hz
✓ 风坐标系 world;地面摩擦/坡度 0.80/30°;碰撞体 mesh;出生点 yaw 90°
✓ 相机 orbit/6.0/2.0;环境光 2.0/雾 0.20;风采样 t=0 等于基础风
✓ 场景 GLB 可下载且魔数正确(glTF,1153244 字节)
✓ 动作内部标准 mediapipe33@0.1.0;客户端解析帧数 2901 == 服务端 2901
✓ 无解析失败行;首帧 33 点;每帧带服务端关节角;30.00 fps;96.67 s
✓ 动作缩略图可下载且为 PNG
✓ 模型格式 vrm、默认模型、文件可下载且 glTF 容器
[integration] ✓ 跨端集成探针全部通过
真实素材
播种用的都是仓库根 示例文件/ 下的真实素材(不是编造的):
【动作】霜雪千年.vmd(827,973 B → 2901 帧内部标准)、
模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip(1.0 MB → VRM,humanoid 19/22、drivable)。
踩坑记录
- autoload 单例在
--script模式下不是全局标识符:AppConfig/CatalogStore直接当名字用会Compile Error: Identifier not found(脚本来自项目外,编译期不认这两个 autoload 名)。改为root.get_node_or_null("AppConfig")取节点后动态调用。 - 协程里一处运行时错误会让进程永不退出:
_finish()不会被调用, 表现为「无输出卡住」,很难定位。已加 120 秒看门狗:超时判失败并退出。 - 输出缓冲:
print()管道输出在进程被 SIGTERM 杀死时会丢, 调试这类脚本要重定向到文件而不是| head。