Commit Graph
39 Commits
Author SHA1 Message Date
zijunchiang 1c5f28cd41 test(codec): 关键点帧跨实现一致性验收——包内 GDScript 解码器 × Web 参考实现(T-WIN-021)
契约 §2 的参考实现是 opendance-web 的 keypoint-codec.ts(其 decodeBinaryFrame
注释就是「Windows 端参考实现」)。Android 已用金标准向量与它对齐;而本端的解码器
此前**只跟自己比过**(自己编码、自己解码),两个方向共用同一份错误假设也能一直绿。

新增:
  * build/integration/frame-vectors.json —— 由参考实现生成并锁定的向量
    (12 用例:i16 最小值 -32768、i16 最大值、±1 全量程、置信度 0/128/255、
    flags 低 3 位组合、frame_id 0xFFFFFFFF 与 0x80000000、取整边界)。
  * build/integration/probe-frame-vectors.gd —— 用**包内** KeypointFrame 解码同一批字节,
    与参考解码结果**逐值精确比对**(坐标/置信度先收敛到 float32 再比精确相等,
    因为本端点存 PackedVector3Array、置信度存 PackedFloat32Array)。
  * build/accept-frame-vectors.sh —— 编排:用参考实现重新生成向量并与锁定文件比对
    (参考实现变了却不同步向量即失败)→ 解包 → 包内解码比对 → 报告核对。

实测(**已发布**的 v0.2.3 资产重新下载后跑):
  ✓ 锁定的向量与参考实现重新生成的结果逐字节一致
  ✓ 解码用例 12/12,逐点比对 396 个,解码差异 0 项
  ✓ 编码器差异 1 项,且全部属于「量化前恰好 .5」的取整边界
  关键点帧跨实现一致性验收全部通过

已测量并归类的一处差异(非缺陷):JS Math.round 与 Kotlin(生产发送方)Math.round
对 -2.5 取 -2(半数朝 +∞),本端 roundi 取 -3(半数远离零)。Windows 是接收方,
encode() 只在自测/协议回归里用,故对产品无影响;脚本硬性要求这类差异只能出现在
取整边界用例上,否则视为新问题。若将来本端要当发送方,把 roundi 换成 floor(x+0.5) 即可。

踩坑(两处「看着通过其实没测到」,都写进任务文档):
  1. 生成器第一版把「喂进编码器的输入」当成 points 写进向量 → 比对变成「比编码量化」,
     靠抽查数值才发现;现在向量同时含 inputPoints / f32InputPoints / f32Hex / points。
  2. 编码器比对一开始用 float64 输入编出的 hex 当基准(正数恰好 .5 那例整例 99 字节不同),
     实际只是输入从 float64 收敛到 float32 的量化差;改用同输入的 f32Hex 基准后,
     差异收敛到唯一一个真正的取整边界用例。教训:跨实现比对必须先统一输入。

package-release.sh 结尾把四条发布前验收一次列全;verify.sh 全绿(774 通过 / 0 失败)。
2026-09-17 05:52:11 +08:00
zijunchiang cb1c293dcf chore(uid): 补探针脚本的 .uid(Godot 首次导入自动生成,与其他脚本一致入库) 2026-09-17 05:33:25 +08:00
zijunchiang 0ffb2d43bb test(ws): WebSocket 契约一致性验收(独立实现 × 包内 PoseServer)+ 故意不合规的负例服务端
tests/test_pose_server.gd 用的是**同引擎内的客户端**(Godot WebSocketPeer),而真实
对端是 Android(Kotlin/OkHttp)——同引擎自测会共享同样的假设(超时判定、分包、
文本/二进制边界、关闭语义、子协议协商),于是「测试全绿」不等于「Android 连得上」。
契约 §7 也写明:静默超时要用保活避免,而不是靠重连掩盖;这句话只有用独立实现的
客户端实测才算验过。

新增三个文件:
  * build/integration/probe-ws.gd —— 把**包内** PoseServer 跑起来(--main-pack +
    只加载包内代码),把连接/控制帧/帧/断开事件汇总成 JSON 报告;自身不做判定
    (避免「用被测实现判定被测实现」)。支持 --stop-file 提前收尾。
  * build/integration/ws-client.mjs —— Node 实现的契约一致性客户端(用内置
    WebSocket,零依赖),逐条跑 §1 §1.1 §2 §3.1-§3.6 §5 §6 §7 的场景。
  * build/accept-ws.sh —— 编排:解包 → 起探针 → 等监听 → 跑客户端 → 收报告 →
    核对客户端看不到的那一半(frames_dropped / malformed_frames / 控制帧计数 /
    device_replaced / 关闭原因 / 静默超时消息带毫秒数与阈值)。

实测(**已发布**的 v0.2.3 资产重新下载后跑):客户端 30 通过 / 0 失败 + 服务端侧
16 项事实全过。关键两条:
  * 保活对照:只发应用层 ping 持续 19 秒(> 静默超时 15 秒)连接**仍保持打开**;
  * 静默超时:完全不发数据 → close(1001,"timeout"),实测 15011 ms(阈值 15000 ms);
另有:子协议协商、welcome 声明保活参数且 heartbeat(5000) < silentTimeout(15000)/2、
pong 原样回传 sentAtMs、同 deviceId 替换旧连接(1000 "replaced by new connection")、
bye → 1000、协议不匹配 → bye + 1002、72 帧含故意跳号(服务端 frames_dropped=3)
与故意 100 字节非法帧(malformed_frames=1)。

反向验证:新增**故意不合规的服务端** build/integration/probe-ws-bad.gd
(welcome 不声明保活字段、不带 sessionId、收到 ping 不回 pong、不做 deviceId 去重),
对同一客户端 → 14 项 ✗、退出码 1,证明断言不是空转。

踩坑记录(已写入任务文档):
  1. --script 模式下从无类型数组取 peer,peer.get_packet() 返回类型推不出来,
     var x := … 直接 Parse Error;
  2. 复核静默超时消息时写了否定式断言 !includes("静默超时 1"),把正确行为判成失败 ——
     断言应写成正向模式匹配;
  3. 探针原本只能按固定时长跑,编排脚本得猜时间;改成客户端跑完 touch stop 文件。

顺带:package-release.sh 结尾把三条发布前验收(产物自证 / WS 契约一致性 / 跨端集成)
一次列全。不影响已发布的 v0.2.3 产物,故不新发版本。
2026-09-17 05:32:24 +08:00
zijunchiang 6238873312 chore(accept): accept-integration.sh 拒绝未知参数
首跑时把 --expect <path> 当成位置参数传进来,被静默忽略(期望值路径只认
EXPECT_PATH 环境变量),于是 --skip-seed 时找不到期望值文件才报错 —— 属于
「参数写错却先跑一半」的糟糕体验。现在除 --skip-seed 外一律报错退出。

不影响发布产物(accept-integration.sh 不在 zip 内),故不新发版本。
2026-09-17 05:21:54 +08:00
zijunchiang a78a536476 feat(integration): 跨端集成验收(包内客户端 × 真实 Web 服务端)+ 修两个真实缺陷(v0.2.3)
单测打的是假 HTTP 夹具,Web 端测试只验自己那一侧 —— 两端各自绿灯不代表契约字段
真的对得上。本版加了跨端集成验收,并在首次运行就抓到两个真实缺陷。

一、跨端集成验收(T-WIN-019)
  * build/integration/probe-integration.gd:只加载**包内**代码
    (`godot --main-pack <pck> --script <探针>` + res://),打真实服务端并逐项断言:
    /health 六个契约版本、catalog/version 指纹、catalog/export 解析出的四类资源与
    defaultModelId;**场景物理参数逐项比对**(重力/基础风/阵风/风坐标系/地面摩擦坡度/
    碰撞体/出生点/相机/环境)+ Wind.sample(0) + 场景 GLB 可下载;
    动作 NDJSON → 客户端解析帧数 == 服务端帧数、33 点、每帧服务端关节角、
    推导帧率 30 fps、时长 96.67 s;缩略图 PNG;模型 VRM 可下载。
  * build/integration/seed-catalog.mjs:用**真实素材**播种目录
    (示例文件/ 下的真 VMD → 内部标准 33 点;真 PMX → VRM;场景 GLB + 物理参数),
    并把期望值(含从 /health 取的契约版本)写成 JSON。
  * build/accept-integration.sh:编排「服务端可达 → 播种 → 包内探针 → 汇报」。
  实测(v0.2.3 包内 pck × 真实 Web v0.7.4 + 真实 MySQL):全部通过。

二、探针抓到的两个真实缺陷
  1. `CatalogTypes.Motion` 缺 `thumbnail_url`:服务端 catalog/export 从
     data-models 0.6.0 起一直在发 thumbnailUrl(Model/Scene 都有),客户端默默丢掉。
     已补字段与解析(缺失为 ""),新增 tests/test_catalog_types.gd(4 项 20 断言)
     用**服务端真实报文形状**锁住三个资源类型的覆盖,并显式记录一处刻意不消费的差异
     (Model.usable/unusableReason —— 目录只下发可用模型,恒为 true)。
  2. `ApiClient._request()` 对**所有**响应无条件 `raw.get_string_from_utf8()`:
     数据本身没错(二进制路径返回原始字节),但每下载一个文件都会打印
     「Unicode parsing error … Invalid UTF-8 leading byte (N)」噪声,并把整个文件
     (模型可达数十 MB)解码成 String 拷贝后丢弃。改为只在需要文本时解码。

三、顺带修掉测试里的写死契约版本
  tests/test_api_integration.gd 断言 rest-api/websocket-keypoints/skeleton-topology
  均为 "0.1.0"。该用例只在服务端可达时才跑,平时一直 skip,所以契约升到 0.5.0 后
  没有任何地方发现它。改为从仓库 contracts/VERSION 读期望值并与 /health 比对
  (顺带成为「客户端契约快照 vs 服务端」的跨端校验)。

证据:verify.sh 全绿(820 通过 / 0 失败,比上版 +46,因为服务端在线时 API 集成组
真的跑了);包内 pck 跨端集成探针全部通过;check-version/PE 版本资源 = 0.2.3。
2026-09-17 05:18:03 +08:00
zijunchiang e4e534f2a2 test(accept): 发布包级端到端验收(读包内 pck 而非源码)+ T-WIN-018
`build/verify.sh` 跑的是仓库源码(`godot --path .`),交付给用户的是 zip 里的
OpenDance.pck + OpenDance.exe,差别全在「导出」这一步:导出预设漏资源、pck 没
重新生成、版本号没同步、契约快照与 pck 不一致 —— 都只在包里才暴露。
本仓 v0.2.1 的 PE 版本元数据就是这么翻的车。

新增:
  * build/probe-release.gd —— 包内运行时探针。用
    `godot --headless --main-pack <pck> --script <探针>` 只读包内资源,断言并
    quit(0/1):
      1) application/config/version == 期望版本(界面版本唯一来源,§11.1);
      2) AboutScreen.app_version() == 期望版本(「关于」清单真正显示的值);
      3) 主场景可加载、四个 autoload 都在场景树里;
      4) 15 个关键资源(UI/模型/场景/网络/评分)在包内都可加载;
      5) 包内 Credits.CONTRACT_VERSIONS 与包内 contracts/VERSION 快照逐项一致
         (抓「pck 用旧契约构建、快照却拷了新的」)。
  * build/accept-release.sh —— 一个 zip(或已解压目录)跑完:sha256 → 解压
    (deflate 用 unzip / LZMA 用 7z,并检查没有 .partN 残留)→ 文件清单 →
    check-pe-version.sh → 包内 pck 启动 180 帧无 SCRIPT ERROR → 探针。
  * package-release.sh 结尾打印下一步验收命令;README 补用法。

实测(对**已发布**的 v0.2.2 资产重新下载后跑):
  sha256 OK;PE 版本资源 0.2.2;包内 pck 启动 180 帧无错误;
  包内 version=0.2.2;AboutScreen.app_version()=0.2.2;主场景
  res://scenes/main.tscn;autoload 齐备;15 项关键资源可加载;
  pck 内契约表与包内 contracts/VERSION 一致 → 发布包级验收全部通过。

反向验证:
  * 篡改包内 contracts/VERSION 的 rest-api=0.4.0 →
    ✗ ["rest-api: pck=0.5.0 快照=0.4.0"],退出码 1;
  * 期望版本写 0.2.1 对 v0.2.2 的包 → 三处版本断言全失败,退出码 1。

顺带修掉脚本自身一个缺陷:SHA256SUMS 用相对路径 + cd 到包目录后被二次拼接成
dist-release/dist-release/SHA256SUMS,导致 sha256 校验假失败(其余检查仍全绿,
很容易被忽略)。首跑时正是它把「全部通过」变成了失败。

verify.sh 仍全绿(754 通过 / 0 失败 / 5 跳过)。
2026-09-17 04:50:40 +08:00
zijunchiang ba988f6ca6 fix(packaging): 发布包 exe 的 PE 版本资源必须等于发布版本(v0.2.2)
缺陷(v0.2.1 实测):把 opendance-windows-v0.2.1.zip 解开、在 Windows 上
「右键 → 属性 → 详细信息」,显示的是

    文件版本    0.1.0
    产品版本    0.1.0

而产物名、Git tag、README 全是 v0.2.1 —— 正是文档 §11.2 要禁止的
「不可核对的发布」。

根因:export_presets.cfg 的 application/file_version /
application/product_version 被硬编码在 0.1.0,而它们**不会**跟随
project.godot 的 application/config/version 自动更新;check-version.sh 当时
只比 project.godot ↔ project.yaml(两处一致),所以静态校验全绿、产物却是错的。
漂移发生在没人看的第三个位置 —— 校验位置少一个,等于没校验。

证据:
  strings -a -el OpenDance.exe  ->  FileVersion 0.1.0 / ProductVersion 0.1.0
  VS_FIXEDFILEINFO @0x67b976c   ->  FileVersion 0.1.0.0 / ProductVersion 0.0.0.0
对照组:官方模板 windows_release_x86_64.exe 的 FileVersion = 4.7.2,
说明本机 rcedit 改写是生效的,问题纯粹在预设的值本身。

修复:
  * build/sync-export-version.sh(新增):导出前把版本号写进导出预设,
    幂等、拒绝非 X.Y.Z;
  * build/check-pe-version.sh(新增):直接读 exe 的 PE 版本资源 ——
    VS_FIXEDFILEINFO(签名 0xFEEF04BD 后 16 字节,grep -abo 取偏移约 40 ms)
    与 StringFileInfo(strings -el,资源管理器属性页显示的就是它)。
    两处都查:实测固定信息里的 ProductVersion 恒为 0.0.0.0;
  * build/check-version.sh:新增导出预设两个键的校验;
  * build/export-windows.sh:导出前同步、导出后直接复核产物;
  * build/package-release.sh:打包前校验源 exe、解包后再校验包内 exe;
  * build/verify.sh:新增步骤 1/5「产物 PE 版本资源」(未导出则跳过);
  * tests/test_about.gd:新增 test_export_preset_version_matches_project_version。

反向验证:把预设 file_version 改回 0.1.0 →
  check-version.sh 退出码 1(报出两处不一致);
  无头测试 753 通过 / 1 失败,失败项正是
  「导出预设的 file_version 与版本号唯一来源一致」。

实测 v0.2.2 产物:PE FileVersion 0.2.2.0 / StringFileInfo 0.2.2,
版本号在 project.godot / project.yaml / export_presets.cfg / exe 元数据 /
产物名 / tag 六处一致。verify.sh 全绿(754 通过 / 0 失败 / 5 跳过)。

同时把 LZMA 压缩的取舍写清楚:deflate 包 38,248,818 B 仍超 IIS
30,000,000 B 上限,故 Release 资产仍是 LZMA 整包(26,628,529 B,不拆分卷);
若上限放开或改由 Web /static 分发,用 ZIP_METHOD=deflate 重打包即可得到
资源管理器可直接双击解压的包。
2026-09-17 04:22:57 +08:00
zijunchiang 14ee5f7ec9 fix(scene): 包围盒沿树累乘变换(修掉入树前的 ERROR 与范围偏位)+ 21 项"真机等价"测试
缺陷:SceneRuntime._compute_aabb() 用 MeshInstance3D.global_transform 计算包围盒,
而它在 _assemble() 里调用 —— 那时运行时节点**还没入树**。两个后果:
- Godot 打一行 `ERROR: Condition "!is_inside_tree()" is true. Returning: Transform3D()`
  (真机每次载入场景都刷一条,与"日志里不应有 ERROR"的排查约定冲突);
- 树外的 global_transform 返回单位变换,算出来的只是各网格的**局部** AABB,
  于是重力/风 Area3D 的碰撞盒与兜底地面会偏小或偏位 —— 对把网格摆在非原点位置的
  场景(导出 GLB 的常见情况)**风力可能作用不到物体上**。

修复:改为从根往下累乘 `Node3D.transform`,不依赖是否入树;挂在单位变换父节点下时
结果与旧语义一致。

新增 21 项无头"真机等价"测试(不需要设备):
- tests/test_scene_config.gd:在代码里手写合法 GLB(三角形 + BIN chunk)落盘后
  **真实加载**:warnings 为空、按 colliders 三种取值分别核对碰撞体(mesh / auto 命中
  *_col / auto 未命中的兜底地面 / none)、重力 Area3D 的 gravity 与方向、
  SPACE_OVERRIDE_REPLACE、风力与 WIND_FORCE_SCALE 对齐,以及**带位移网格**
  (translation [10,0,0])时重力区域与兜底地面都跟着位移(该缺陷的回归测试);
- tests/test_pose_server.gd:**时间维度**验证保活 —— 只发应用层 ping、不发关键点帧的
  客户端在 17 秒(> 15 秒静默超时)后仍保持连接("画面里没人就掉线、疯狂重连"的根因
  对照),而完全不发数据的客户端被 close(1001) 断开。

新增 build/check-version.sh:强制 project.godot 的 application/config/version
(「关于」清单与 .pck 的来源)与 project.yaml、发布版本号三者一致;已接入
build/package-release.sh(打包前)与 build/verify.sh(第 0/4 步),并做过反向验证。
此前没有任何机制保证这两个文件同步 —— 界面显示的版本可能和产物名不一致。

版本升至 0.2.1(project.godot / project.yaml / README / 产物名 / tag 全部一致),
About 清单与 .pck 内版本号均为 0.2.1(已用 strings 核对)。

验证:build/verify.sh 四步全通过(版本一致性 → 编译 → 751 项断言 → 180 帧启动无错误);
产物 opendance-windows-v0.2.1.zip(LZMA,26,627,191 B)
sha256 d49885327047f5a52de6c732431ea080cd1a030ae1c581a8bc0ec0d99bfb792c。
2026-09-17 03:59:29 +08:00
zijunchiang 86e5decd10 build(release): 增加 ZIP_METHOD=lzma 选项(37 MB 整包被 IIS 30 MB 上限挡住时用)
Windows v0.2.0 发布时,Gitea 站点 IIS 的 maxAllowedContentLength 仍是默认的
30,000,000 字节:16.4 MB 通过,而 38,245,602 B 的整包用**正确**的 multipart 写法
(字段名 attachment)复测依旧 HTTP 413(仅发出 917 KB 即被拒)。为不阻塞发布,
本次给打包脚本增加压缩方式开关:

  ZIP_METHOD=deflate   (默认)约 38 MB,Windows 资源管理器双击即可解压
  ZIP_METHOD=lzma      约 26 MB,可绕过 30 MB 闸;**代价**是 Windows 10 资源管理器
                       不支持 LZMA(method 14) 的 zip,必须用 7-Zip / WinRAR / PeaZip

脚本细节:
- lzma 走 `7z a -tzip -mm=LZMA -mx=9`(缺 7z 时报错并给出安装提示);
- lzma 包额外做 `7z t` 完整性测试,并改用 `7z x` 做解压自检(unzip 对 method 14
  支持不一致,不能用来验证);
- 超限告警里补上「可用 ZIP_METHOD=lzma 绕过」的提示。

README.md / README.txt 同时写明:v0.2.0 的 zip 是 LZMA,需要 7-Zip/WinRAR 解压;
若 IIS 上限已放开,请改用默认 deflate 包。
2026-09-17 03:42:05 +08:00
zijunchiang 76244d2d0d docs(tasks): 更正「Gitea 附件通道坏了/磁盘写满」的误判,并记录正确的上传写法
排查 Windows 整包上传时的 HTTP 500 曾被判成"Gitea 宿主机磁盘写满",**这个结论是错的**:
Gitea 的 Release 附件接口 multipart 字段名是 **`attachment`**,用 `file=` 会得到

  HTTP 500 {"message":"","url":"https://gitea.kasugano.cn/api/swagger"}

空消息,极易误判成服务器故障。同一 1 KB 文件、同一 Release 的实测对照:
  -F "attachment=@probe.bin"                → 201
  --data-binary @probe.bin(带 ?name=)      → 201
  -F "file=@probe.bin"                      → 500(空消息)

**真实且仍然存在的唯一限制**:IIS `maxAllowedContentLength`(默认 30,000,000 字节)——
927 KB / 14 MB / 16,372,427 B 通过;30.5 MB / 38,245,602 B / 40 MB 一律 **HTTP 413**
(本轮用正确字段名复测整包:413,仅发出 917 KB 即被拒)。

tasks/014-full-package.md 的「实测记录」改写为两段:真实限制 + 误判更正,
并附两种可直接照抄的上传命令(multipart `attachment` / 裸 body + `?name=`)。
2026-09-17 03:40:27 +08:00
zijunchiang d9229fae5f docs: 补齐部署必需的 Gitea 域名与属主;重新打包 v0.2.0(含修正后的说明文档)
域名与仓库属主属于「部署必需且非隐私」的信息,此前写成 `<Gitea 地址>/<owner>`
会让读者无法照抄。补成真实值:gitea.kasugano.cn / zijunchiang
(口令与访问令牌仍为占位符)。

- README.md / README.txt:clone 地址与跨仓库链接补成真实地址;
- README.md / README.txt:说明 `<服务器地址>` 是你们自己部署的 Web 服务端地址
  (部署方决定,不是被脱敏的域名),示例里的 192.0.2.10 为 RFC 5737 文档用 IP。

因本仓库 v0.2.0 的整包**尚未上传**(Gitea 附件通道故障),本次一并按修正后的
文档重新打包,避免发布出一个域名仍是占位符的包;新包的 sha256 会更新到
Release 说明里。
2026-09-17 03:07:24 +08:00
zijunchiang cbd5001e98 docs(tasks): 修正整包上传失败结论——与体积无关,指向 Gitea 宿主机磁盘
原先记的「16.4 MB 边界」是误判(当时磁盘正被失败的请求逐步填满)。补做隔离
测试后:1 KB 与 100 KB 也失败且是**瞬间**失败(不是读完请求体才判断),三个仓库的
Release 资产接口同样失败,而创建 Release / PATCH 说明(纯 JSON)都正常,
其间还有 927 KB 的附件成功过一次 —— 因此不是体积上限,而是「附件文件落盘」这条
通道坏了,最可能是 Gitea 宿主机磁盘/临时目录几乎写满(返回 500 而非 413)。

IIS 那一关已确认放开生效(38,245,661 字节请求体被完整接收,不再 413)。
整包与 SHA256SUMS 已就绪,待宿主机空间问题解除后立即上传(不生成分卷)。

详见 opendance-web/deploy/proxy/README.md 第九节 9.1。
2026-09-17 01:51:33 +08:00
zijunchiang e4b33a9c98 docs(tasks): 记录 v0.2.0 整包上传的两阶段实测(IIS 已放开,Gitea 侧仍 500)
按文档 §5.7 硬性要求 4 记录实测上限:
- IIS 未放开时 30.5 / 40 MB 返回 413;
- IIS 放开后,38,245,661 字节的整包请求体被**完整接收**(115 s),
  但 Gitea 返回 HTTP 500(API 错误信封 message 为空);
- 同接口边界:914 KB 与 16,372,427 B 通过,16,400,000 B 及以上失败;
  多次大包失败后连 100 KB 都会在 0.3 秒内 500 —— 更像上传临时目录 /
  数据盘写满(Gitea 先写 data/tmp),而不是体积上限。

结论:只放开 IIS 不足以放行 38 MB 整包,还需清理/扩容 Gitea 数据盘
并调整 app.ini 的 [attachment] MAX_SIZE;详见
opendance-web/deploy/proxy/README.md 第九节。
2026-09-17 01:40:33 +08:00
zijunchiang c3a227c6df feat(net): 连接保活 / 设备去重 / 连接状态机(T-WIN-015,契约 websocket-keypoints 0.2.0)
与 Android 端 T-AND-011「两端一起改」:契约先在 opendance-web 修改并提升到
websocket-keypoints 0.2.0(向后兼容:子协议名与二进制帧 version 均不变,
只新增可选控制字段与一种新控制类型),再同步三端快照。

修掉本端的 3 处根因/放大器:
1. 静默超时 5s 过短且未声明,客户端无从得知。OkHttp 的协议层 ping 由底层库
   自动应答、**不暴露给应用层**,无法刷新 last_seen_ms,于是"画面里没人"时
   每 5 秒必然踢一次连接 -> 客户端重连 -> 再被踢。
   - CLIENT_TIMEOUT_MS 5s -> 15s,新增 HEARTBEAT_INTERVAL_MS = 5s;
   - welcome_payload() 在 welcome 中声明 silentTimeoutMs / heartbeatIntervalMs,
     config 广播同样带上(契约 §1.1:心跳必须 < 静默超时的一半,5000*2 < 15000);
   - 处理应用层 ping 并回 pong(回显 sentAtMs + serverAtMs,供对端算 RTT)。
2. 反复重连被计成多台设备。按契约 §5 用 hello.deviceId 去重:同一设备只保留
   最新连接,旧连接以 close(1000) 关闭——刻意用 1000,因为对端按契约 §7
   收到 1000 不会重连,否则会形成"挤掉-重连-再挤掉"的循环;
   新增 device_count() 与 device_replaced 信号,界面一律显示去重后的设备数。
3. 只有 handshaken 布尔量,日志看不出卡在哪一步。按契约 §8 逐客户端维护与
   Android 端同名的状态(connecting -> handshaking -> streaming -> degraded,
   主动停止/超时/被替换为 stopped),并记录 close code/reason、协商子协议、
   连接/握手/首帧时间;degraded 保持时长做成可覆盖字段便于测试。

验收测试(tests/test_pose_server.gd 新增 7 项,总计 720 通过 / 0 失败):
welcome 声明保活参数、ping->pong 回显、同 deviceId 去重(旧连接 1000 关闭且
设备数仍为 1)、8 个状态名与契约一致、状态随握手/首帧推进到 streaming、
丢帧进入 degraded 后恢复、snapshot 暴露设备数与保活参数。

同时更新 README/README.txt/tasks 与 Credits 的契约版本(0.2.0)。
2026-09-17 01:11:48 +08:00
zijunchiang 5581cbe05d feat(scene): 场景选择与物理落地 + 整包发布(T-WIN-012/013/014)
T-WIN-013:API 升级与场景选择
- 契约快照同步到 rest-api 0.5.0 / data-models 0.7.0 / database 0.4.0
- catalog_types.gd 新增 SceneConfig(物理/环境/出生点/相机)与 Scene;
  CatalogBundle 增加 scenes[];from_dict 按契约 7.1 再收敛一次
  (缺项默认、越界夹紧、非法枚举回退),与服务端 normalizeSceneConfig 同规则
- 新增 src/scene/scene_runtime.gd:GLB 载入 -> 碰撞体(mesh 三角网 /
  auto 命名约定 *_col、*_colonly / none 不兜底)-> 场景级重力(Area3D
  SPACE_OVERRIDE_REPLACE,不改全局 ProjectSettings)-> 风(每物理帧
  apply_central_force,阵风正弦叠加,支持 local 坐标系)-> 环境光与雾
  -> 出生点与相机(follow/fixed/orbit);无碰撞体时兜底地面
- CatalogStore 增加 scenes/default_scene/find_scene/ensure_scene_cached/
  fetch_thumbnail(场景缓存带 sha256 指纹,避免换包后仍用旧 GLB)
- 主界面改为 2x2 四个选择器(曲目/动作/模型/场景)+ 场景参数面板
  (缩略图 + 重力/摩擦/弹性/碰撞体/风摘要)
- 游戏界面载入场景并落地物理,角色落在出生点;场景缺失/下载失败/
  解析失败一律回退内置空场景并提示,不阻塞开始跟练

T-WIN-012:「关于」清单展示当前版本号
- 版本唯一来源为 project.godot 的 application/config/version(v0.2.0)
- about_manifest() 输出版本/引擎/契约/协议/地址/隐私清单;
  无头测试无 autoload,读取 AppConfig 走 get_node_or_null 容错
- Credits 的单个 CONTRACT_VERSION 常量改为 CONTRACT_VERSIONS 字典
  (六项契约各自独立递增),并双向核对 contracts/VERSION

T-WIN-014:发布必须是单个完整包
- build/package-release.sh 重写:只产出单个 zip + SHA256SUMS,
  清理历史 .partN,超限时告警而非回退分卷;新增源产物与解压后完整性
  自检(PE 头 MZ / pck 魔数 GDPC / exe+pck 同目录同名)与体积-上限对比
- deploy/proxy 相关的 Gitea IIS 256MB 上限说明见 opendance-web

测试:tests/test_scene_config.gd(12 例)+ tests/test_about.gd(4 例);
无头测试 687 通过 / 0 失败(10 个套件),verify.sh 全部通过
2026-09-17 00:45:23 +08:00
zijunchiang 8c1d24865c docs: 全角 | / ? 改 ASCII(Gitea 混淆字符检测的剩余触发项) 2026-09-15 21:36:41 +08:00
zijunchiang 68a5ca0a87 docs: 其余易混淆字符(x/>=/<=/~/OK 取代 × − ≥ ≤ ≈ ✓) 2026-09-15 21:34:50 +08:00
zijunchiang 9f7b926a9a docs: 新增「初次部署(从零跑起来)」;去混淆字符与真实内网信息
- README.md / README.txt 新增初次部署章节:前置条件、获取代码、首次运行、
  配置服务端地址(API 3000 / 管理端 3001 分离)、与 Android 的 WebSocket 联通、
  打包导出、自检清单、常见问题
- 三库同源契约:':=' 写法修回、去掉真实 MariaDB 版本串
- 全文去除易被 Gitea 判为「模棱两可 Unicode」的字符(- - ... -> 取代 — – … § →)
- 真实内网地址与库名改占位符
2026-09-15 21:33:27 +08:00
zijunchiang baec8fce97 chore(contracts): 同步 rest-api(新增 POST /api/models/{id}/reanalyze) 2026-09-15 21:18:21 +08:00
zijunchiang 5a13167739 chore(contracts): 同步契约到 0.4.0/0.3.0(GLB/VRM 主力格式、模型审核与骨骼分析) 2026-09-15 20:37:58 +08:00
zijunchiang c22790f3fe chore(contracts): 同步契约(默认模型改用压缩包 sha256 匹配,补 sourceSha256 与确定性说明) 2026-09-15 19:36:42 +08:00
zijunchiang f4cf2c70de chore(contracts): 同步契约到 rest-api/data-models/model-formats 0.3.0、database 0.2.0(模型压缩包转换与默认模型规则) 2026-09-15 13:55:59 +08:00
zijunchiang 8f303c0595 chore(contracts): 同步契约到 v0.2.0(rest-api / data-models / model-formats)
Web 端需求变更带来的契约字段级改动:
- MotionFormat 枚举新增 vmd(MikuMikuDance 动作数据)
- rest-api 服务地址说明改为双端口语义(软件 API 端口给客户端 / 管理端端口给浏览器)
- model-formats 新增 §2.3 VMD 解析约定(标准 MMD 骨架、腿部 IK/FK 判定、手性转换)

三库契约文件保持逐字节一致。
2026-09-15 08:45:06 +08:00
zijunchiang 3ba8103723 docs(contracts): 服务地址描述由 Docker 部署改为 Linux 业务服务部署(Web 端已去 Docker) 2026-09-15 01:27:50 +08:00
zijunchiang aef5f88083 docs(win): 用实现说明替换阶段二占位 README 2026-09-14 20:05:40 +08:00
zijunchiang c8dd6b2d4c chore(win): 合并占位说明与阶段二实现 2026-09-14 20:05:17 +08:00
zijunchiang fe9608eba7 chore(win): 提交 Godot 生成的 .uid 与资源 .import 元数据 2026-09-14 20:05:04 +08:00
zijunchiang a83e4f6a1f docs(win): 任务说明、DSH 约束与交付文档 2026-09-14 20:04:54 +08:00
zijunchiang 315cf79708 feat(win): T-WIN-011 Windows 打包与一键验证脚本 2026-09-14 20:04:54 +08:00
zijunchiang 13d6dc915d feat(win): T-WIN-008/009/010 主界面/游戏/结算/来源展示/关于页 2026-09-14 20:04:54 +08:00
zijunchiang 7776f68f0b feat(win): T-WIN-007 特征提取 + 流式 DTW 评分引擎 2026-09-14 20:04:54 +08:00
zijunchiang c092ed1628 feat(win): T-WIN-006 关键点 → VRM 骨骼重定向 2026-09-14 20:04:54 +08:00
zijunchiang 0ae1c101a7 feat(win): T-WIN-005 VRM 加载 + humanoid 骨骼名映射 2026-09-14 20:04:54 +08:00
zijunchiang 2e539c7fa6 feat(win): T-WIN-003 WebSocket 关键点接收服务 2026-09-14 20:04:54 +08:00
zijunchiang 787f1c0cd8 feat(win): T-WIN-002 REST 客户端与契约类型 2026-09-14 20:04:54 +08:00
zijunchiang ad62929f59 feat(win): T-WIN-004 239 字节关键点帧编解码 + 无头测试框架 2026-09-14 20:04:54 +08:00
zijunchiang 85a2568266 feat(win): T-WIN-001 Godot 工程骨架 + 4 个 autoload 2026-09-14 20:04:54 +08:00
zijunchiang 0b7083d026 chore(contracts): 引入 opendance-web v0.1.0 契约只读快照 2026-09-14 20:04:53 +08:00
zijunchiang 20f27271a8 docs: 初始化占位说明与契约依赖 2026-09-14 19:02:38 +08:00