fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s

- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
This commit is contained in:
2026-10-21 09:25:17 +08:00
parent 6a0eb14b28
commit 054379dc64
52 changed files with 1070 additions and 280 deletions
+19 -19
View File
@@ -29,7 +29,7 @@
| `immersive` | F11 | chrome 隐藏;指针移到上沿 8px 时 TopBar 150ms 淡入,3 s 后淡出;指针静止 2 s 隐藏光标 |
| `broadcast` | 视图切换 `Ctrl+Shift+B` | 只渲染 `BroadcastStage`;禁用设置/维护/发现列表;不渲染任何 IP/token |
结构落点:`App.tsx` 里现有的门禁分支(致命错误 / 等待快照 / 身份选择 / 设密码)不动,只在最后一段把 `TeamPage | RefereePage` 包进 `AppShell`,并新增 `BroadcastPage` 分支。
在 `App.tsx` 中保留现有门禁分支(致命错误 / 等待快照 / 身份选择 / 设密码),只在最后一段将 `TeamPage | RefereePage` 放入 `AppShell`,并新增 `BroadcastPage` 分支。
### 用户端 `TeamPage`(操作手视角)
@@ -60,7 +60,7 @@
### 裁判端 `RefereePage`
三区 + 一条比赛条:
裁判端分为三区,另设一条比赛条:
```
┌ TopBar ────────────────────────────────────────────────────────────────┐
@@ -84,7 +84,7 @@
2. `referee-stage` 保持 `flex: 1`,新增的 `MatchBar` 占固定高度;下方控制台采用可拖拽高度(提议默认 240px,最小 120,最大 45vh);加一行比赛条后,900px 高的窗口约剩 560px 用于画面。
3. `ArmorTable` 改为两队并列(左红右蓝),每行左侧加 5 格迷你血条(与 HUD 同源,便于核对);主装甲板加 `主` 徽标;离线行 `opacity .55`(保留);`reportsAllPlates=false` 时显示"仅报 2 块"——规则允许 2 或 4 块装甲板。
4. 新增 `EventPanel`(Dock 第 3 个页签):命中/暂停/判负/录制开始/结束的时间轴列表,可导出。
5. `MediaPanel` 保留全部功能,新增两个东西:按局分组(`matchId` 目录)与 "回放本局"(双路同步 + 事件定位)。
5. `MediaPanel` 保留全部功能,新增按局分组(`matchId` 目录)与“回放本局”(双路同步 + 事件定位)两项功能。
6. 队名编辑保留(现在是内联输入),但改成"点击铅笔 → 行内编辑 → Enter 保存 / Esc 取消",避免一直占一行高。
### 直播 `BroadcastPage`(新)
@@ -110,15 +110,15 @@ interface AppStore {
}
```
事件传输:比赛事件走独立推送,与现有的 `hit` 同一条路(`server::WireMsg` 新增 `MatchEvent`),前端累积;`Snapshot.match` 只带最近 12 条用于重连后补齐。理由:快照 5 Hz × 200 条事件会让每份快照增加到几十 KB,事件单独推送可避免重复传输。
比赛事件采用独立推送,与现有 `hit` 使用相同的传输方式:在 `server::WireMsg` 中新增 `MatchEvent`,由前端累积。`Snapshot.match` 仅携带最近 12 条,供重连后补齐。若按快照 5 Hz × 200 条事件传输,每份快照会增加到几十 KB;单独推送可避免重复传输。
计时以后端为准:`Snapshot` 增加 `nowMs: u64`(后端 Unix 毫秒),前端用 `useServerClock` 每帧算 `serverNow = localNow + clockSkewMs`,再用 `match.remainingMs - (serverNow - snapshotAt)` 插值。`setInterval` 在窗口失焦时会被节流,不能作为比赛计时依据。
计时以后端为准。在 `Snapshot` 中新增 `nowMs: u64`,表示后端 Unix 毫秒;前端通过 `useServerClock` 每帧计算 `serverNow = localNow + clockSkewMs`,再用 `match.remainingMs - (serverNow - snapshotAt)` 插值。`setInterval` 在窗口失焦时会被节流,不能作为比赛计时依据。
### 一键判负
入口:`MatchBar` 右侧 `⚑ 判负`,或 `Ctrl/Cmd+Shift+F`。只在裁判端、`phase ∈ {running, paused}` 时可用。
三步,不可合并
以下三步不可合并:
```
① 点按 → ForfeitDialog
@@ -144,7 +144,7 @@ match_end { reason: Forfeit { loser: 0|1, kind: ForfeitKind, note: Option<String
- 状态 → `Ended { by: Referee, reason, at, actor }`,比分按规则 +1 给胜方。
- 用户端若已同步(见 R2):显示 `JudgedOverlay`「本局判负」;未同步则只影响裁判端与直播。
- 不改硬件血量:`ArmorHealthStore` 只接收上报,没有任何写入口(见 R1)。界面必须显式写明这一点,否则现场会以为"判负之后对方血量归零了"。
- 不改硬件血量:`ArmorHealthStore` 只接收上报,没有写入口(见 R1)。界面必须写明这一点,避免现场误以为判负会使对方硬件血量归零。
- 审计:事件 `{ at, actor, action:"forfeit", loser, reason, note, hp:[76,100], remainingMs:161000 }`,追加到 `matches/<yyyy-mm-dd>/<matchId>/events.ndjson`,同时进 `snapshot.match.events`。
- 撤销:`match_undo { eventId }`,5 s 内可直接撤销;之后只能 `match_reopen_round { reason }`,在历史里留下"更正"两条记录(不删除原记录)。
@@ -177,7 +177,7 @@ end: { by: "auto" | "referee"; reason: "knockout" | "timeout" | "forfeit" | "equ
### 计时 / 比分
两个不同的 3 分钟,界面必须分开写
赛前准备与单局比赛各为 3 分钟,界面必须分别标注:
| 阶段 | 时长 | 文案 |
| --- | --- | --- |
@@ -253,7 +253,7 @@ impl MatchClock {
### 基准画布与安全区
基准 1920×1080(16:9)。相机是 640×480(4:3),所以直播层必须自己决定是加信箱还是裁切,不能指望画面填满。
基准画布为 1920×1080(16:9),相机画面为 640×480(4:3)。两者比例不同,直播层需明确选择留边或裁切,不能默认画面会填满画布。
| 区域 | 距边距 | 1080p 像素 | 内容 |
| --- | --- | --- | --- |
@@ -328,7 +328,7 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
- `BroadcastStage` 用 1920×1080 的绝对坐标系 + `transform: scale(min(vw/1920, vh/1080))`,`transform-origin: top left`。这样所有设计稿坐标直接写进 CSS,无需换算为百分比。
- 参考线由 `GuideLayer` 画(`outline: 1px dashed`),受 `props.guides` 控制;全屏/输出时自动关闭。
- 视频源仍用 `VideoSurface`(`ViewTransform` 固定 `IDENTITY_VIEW`),只是容器换成 800×600 的固定框——不要为直播再写一套 canvas 绘制。
- 视频源继续使用 `VideoSurface`(`ViewTransform` 固定 `IDENTITY_VIEW`),将容器改为 800×600 的固定框,复用现有 canvas 绘制逻辑。
### 输出方式
@@ -341,7 +341,7 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
实施时需检查:
1. WebView 背景:`transparent: true` 在 Windows WebView2 与 macOS WKWebView 行为不一致,直播场景下透明边缘会出黑边。结论:用不透明底色 `--bg-void` 铺满,不做透明。
2. 低帧画面 + 高帧包装:视频是 JPEG 序列,帧率取决于固件配置(`commands.rs` 的注释里记着"The sensor 实测 22 fps",`target/firmware/BUILD-INFO.json` 里 vga30 档实测 29.7 fps);无论哪个值都远低于包装层的 60 fps,动效会"飘"在画面上。所以包装层只做状态迁移动画,不做连续动画。
2. 视频与包装层帧率不同:视频是 JPEG 序列,帧率取决于固件配置(`commands.rs` 的注释记录“The sensor 实测 22 fps”,`target/firmware/BUILD-INFO.json` 中 vga30 档实测 29.7 fps),均远低于包装层的 60 fps。连续动效会显得与视频节奏不一致,因此包装层只使用状态迁移动画,不使用连续动画。
3. 纯黑编码:`#000` 在 H.264 里容易 banding,用 `#07080a` + 2% 噪点。
4. OBS 窗口采集会采到鼠标:全屏时 `cursor: none`(`immersive` 状态负责)。
5. 窗口标题:直播窗口标题里不要出现队名/端口等内部信息(`setTitle` 现在会写身份;broadcast 视图下改成 `cam-desktop — 直播`)。
@@ -350,9 +350,9 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
### 直播视图的安全边界(必须做)
- `broadcast` 视图不渲染:设置按钮、维护入口、`cameras[].addr`(IP)、`stream_info.url`(token)、密码相关 UI、装甲板 id 全文(只显示队名)。
- 所有管理命令在广播态禁用(不是隐藏,是 `disabled` + `aria-hidden`,防止误触与 Tab 聚焦)。
- 广播态下需禁用所有管理命令,使用 `disabled` + `aria-hidden` 防止误触与 Tab 聚焦,不能只隐藏入口。
- 演示模式(`demo`)在直播里显示红色角标"演示画面",与现有 `tag--demo` 同一语义(现在只在 TopBar,直播态必须另有一处)。
- 断流时 `StandbyCard` 明确写"信号中断 · 正在重连",不显示上一帧冻结画面冒充直播(冻结画面会被观众当成"画面卡住")。
- 断流时,`StandbyCard` 显示“信号中断 · 正在重连”,不显示上一帧冻结画面,以免观众将其误认为直播卡顿。
---
@@ -377,11 +377,11 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
### 对客户端的影响
1. 血量必须同时有数字和格子。100 点 / 5 格 = 20 点/格,而单发只扣 3 点——一格的粒度相当于 6.7 发。只给格子,操作手无法判断"再中两发就死"。→ 保留 `showNumbers` 默认开启,且数字 ≥ 18px。
2. 100 点 = 34 发(33 发后剩 1 点)。裁判端可以显示"已中 33 发"这类辅助读数(`100 - hp) / 3`),但不要把它当成权威值——静默期内的连击会被合并。
1. 血量需同时显示数字和格子。100 点 / 5 格 = 20 点/格,单发只扣 3 点,一格相当于 6.7 发;仅凭格子,操作手无法判断再中两发是否会归零。因此保留 `showNumbers` 默认开启,且数字 ≥ 18px。
2. 100 点 = 34 发(33 发后剩 1 点)。裁判端可显示“已中 33 发”等辅助读数(`100 - hp) / 3`),但静默期内的连击会合并,不能将该读数作为权威值。
3. 100 ms 静默期决定命中动效下限:同一次攻击可能在 4 块板上各上报一次,`HealthTracker` 按血量差派生,已经能合并;UI 侧不要为每个 board 事件各弹一次伤害数字(否则一次命中会闪 4 下)。
4. 装甲板 2 块也要能显示:`ArmorBoardDto.reportsAllPlates` / `platesSeen` 已存在,界面在 `platesSeen < 4` 时显示"仅报 N 块",不要显示 4 个空槽让人以为板子坏了。
5. "最快开始移动"没有数据源。图传只给画面,协议里没有位移/速度;因此这条规则只能人工判定(见“计时 / 比分”)。不要试图从画面里推算。
5. 目前没有“最快开始移动”的数据源。图传只提供画面,协议不包含位移或速度,因此只能人工判定(见“计时 / 比分”),不从画面推算。
6. 赛前 3 分钟是"确认客户端连上了"的窗口,所以用户端需要一个准备检查卡:图传在线、装甲板在线、帧率、最近一帧年龄、镜像是否正确。这张卡在 `Ready` 阶段自动出现,`Running` 后消失。
7. 红蓝配色固定:队号 0 = 红、1 = 蓝,与部署点/装甲板灯色一致(现有 `teamColor` 已是这个语义,只改色值)。
@@ -439,7 +439,7 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
| 设备故障(图传中断)导致无法比赛 | 人工裁定:技术暂停 / 判负 / 重赛 | 主裁判 |
| 技术暂停超时(>5 分钟) | 提示超时,不自动判负 | 主裁判 |
> 手册 1.0.0 的更新日志提到"添加了详细的扣血、判罚规则与单局时间",但当前公开页面只给出了 上方规则表列出的内容(扣血 3 点、技术暂停 5 分钟、四条胜负条件)。如果组委会另有一份判罚细则(红黄牌、犯规等级),需要提供原文,否则界面不写死任何罚则。
> 手册 1.0.0 的更新日志提到“添加了详细的扣血、判罚规则与单局时间”,但当前公开页面只提供上表所列内容(扣血 3 点、技术暂停 5 分钟、四条胜负条件)。若组委会另有红黄牌、犯规等级等判罚细则,需提供原文;在此之前,界面不写死罚则。
---
@@ -469,7 +469,7 @@ pub enum MsgType { Video = 1, Subscribe = 2, Hello = 3, Heartbeat = 4, ArmorHeal
```
- 裁判端以 `REFEREE_ID` 广播到 `5601`;用户端在装甲板监听里增加一个 `MsgType::MatchControl` 分支(不改相机订阅逻辑)。
- 安全:局域网里任何人都能发 UDP,所以必须带共享码派生 MAC(检录时裁判端生成 6 位码,双方在客户端输入),否则队伍可以伪造"对方判负"。
- 安全:局域网内任何人都能发送 UDP,因此消息必须携带共享码派生的 MAC。检录时由裁判端生成 6 位码,双方在客户端输入;否则队伍可以伪造对方判负消息。
- 固件兼容性待验证:装甲板固件只认 `MsgType::ArmorHealth`,未知类型按现有 `decode_header` 逻辑返回 `None` 被忽略;但上线前必须在实机上验证"未知 MsgType 不会让板子异常"。
### 需要用户确认的问题
@@ -527,7 +527,7 @@ pub enum MsgType { Video = 1, Subscribe = 2, Hello = 3, Heartbeat = 4, ArmorHeal
## 附录 C:范围
1. 不做网络 OTA / 设备自升级(与[固件维护](../maintenance/firmware.md) 的边界一致)。
2. 不做修改固件源码;打包只组装已有产物。
2. 不修改固件源码,打包只组装已有产物。
3. 不做从画面推算"移动先后"、命中位置、弹道等规则数据。
4. 不做直播的透明背景/虚拟摄像头/NDI。
5. 不做多语言(中文界面为唯一语言,符合约束)。