# 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` 一致 | ## 实测(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`)。 ## 踩坑记录 1. **autoload 单例在 `--script` 模式下不是全局标识符**: `AppConfig` / `CatalogStore` 直接当名字用会 `Compile Error: Identifier not found` (脚本来自项目外,编译期不认这两个 autoload 名)。改为 `root.get_node_or_null("AppConfig")` 取节点后动态调用。 2. **协程里一处运行时错误会让进程永不退出**:`_finish()` 不会被调用, 表现为「无输出卡住」,很难定位。已加 120 秒**看门狗**:超时判失败并退出。 3. **输出缓冲**:`print()` 管道输出在进程被 SIGTERM 杀死时会丢, 调试这类脚本要重定向到文件而不是 `| head`。