Files
mqttx/docs/deep-analysis-2026-09-30.md
T
JoeAllen 5ad4536703 docs: 项目深度分析报告(架构/问题清单/修复记录)
只读分析:架构与并发模型、34 项问题清单(P0/P1/P2 分级 + 可信度标注)、发布链路/OTA 信任模型/性能专项、测试覆盖评估、README 差异、修复路线图;0.4/0.5 节记录两批修复与验证证据。
2026-09-30 10:16:45 +08:00

50 KiB
Raw Blame History

MQTTX(GPUI 重写版)项目深度分析报告

  • 分析对象:mqttx 工作区(唯一成员 gpui-app),HEAD = bc9f55c("refactor(ui): 拆分 app/connection_view/widgets 为子组件"),工作区干净
  • 分析日期:2026-09-30
  • 分析性质:分析阶段全程只读(未修改/创建/删除任何源文件)。分析完成后按第 0.4 节实施了批次 0 修复,改动清单与验证证据见该节
  • 验证环境:Windows / rustc 1.101.0-nightly (d080e7dff 2026-09-27) / cargo 1.101.0-nightly
  • 方法:全量通读 gpui-app/src 48 个源文件 + CI/发布脚本/依赖清单;三路并行模块复审(OTA 与阿里云 / UI 表单与对话框 / 支撑模块与工程化);对复审结论逐条回归源码复核
  • 可信度标注约定:
    • ✅ 实测量:本机实际运行命令得到的结论
    • ✅ 代码读取:直接读到源码或依赖源码得到的结论
    • ⚠️ 依赖语义:结论依赖第三方库的文档/源码语义,未做实验
    • 🟡 需实测:无法在离线环境判定,必须用真实环境或官方测试向量核对
    • ❌ 未采信:证据不足或存在相反证据,本报告不作为缺陷传递

目录


0. 摘要

0.1 一句话结论

代码底子明显高于平均线,但工程化外圈是全项目最脆弱的部分。 分层清晰、竞态处理有针对性设计、持久化语义正确;然而 CI 从未在 Linux 上跑通过、发布默认路径会造出非 semver 版本、Windows 产物靠本地脚本手工补、OTA 的信任边界只有 TLS 一道。当前最大的问题不是"写得对不对",而是"发不出去、也升不上来"。

0.2 最紧急的 5 件事(批次 0,约半天)

# 事项 证据 后果
1 Linux 单测确定性失败 + test job 缺图形库依赖 sysmon.rs:194 + release.yml:39-46 CI 永远红,无任何产物 —— ✅ 已修复(0.4)
2 发布脚本用 git describe --tags --always 当版本 publish-gitea.sh:71-78 造出 v1.0.0-4-gbc9f55c 这类 Release,客户端判定"不新",且遮蔽真更新 —— ✅ 已修复(0.4)
3 版本双真源(tag vs CARGO_PKG_VERSION)无校验 Cargo.toml:3 + release.yml:33 忘 bump → 装完仍自报旧版 → 无限提示更新 —— ✅ 已修复(0.4)
4 Release 创建/上传失败被静默吞掉(exit 0) publish-gitea.sh:174-181 job 变绿而 OTA 通道为空 —— ✅ 已修复(0.4)
5 非 Windows 自更新缺可执行位,必失败 install.rs:92、220 Linux 用户永远装不上 —— ⏳ 待批次 1

0.3 问题分布

级别 数量 代表
P0 阻断发布 / 安全边界 6 7z 路径穿越、事件总线无背压、发布链路三不通
P1 正确性 11 MQTT5 收到 0 值直接断连、主题键名不生效、TOCTOU
P2 一致性 / 工程化 22 对话框栈语义、README 漂移、死代码
未决 需实测 2 阿里云签名 content、securemode 与传输层矛盾

0.4 本次已修复项(2026-09-30,批次 0)

分析完成后实施了批次 0 的修复,改动集中在 CI/脚本/测试与文档,未触碰任何业务逻辑:

对应问题 改动 文件 验证证据
P0-1 采样测试改为分平台断言(if cfg! 而非无条件 > 0,两个分支都参与编译) gpui-app/src/sysmon.rs Windows cargo test --lib 33 passed;clippy --all-targets 零告警
P0-1 test job 补齐 GPUI 的 Linux 系统依赖;命令改为 --lib --tests(集成测试只编译不执行) .gitea/workflows/release.yml YAML 解析无错误;test job 4 个 step 结构正确
P0-2 版本解析改用 git describe --tags --exact-match + Cargo.toml 回退;新增 semver 强制校验 scripts/publish-gitea.sh、.ps1 实测:无参数时回退 1.0.0(不再产出 v1.0.0-4-gbc9f55c);显式传该串被拒绝且不发生网络请求;v1.0.1 通过校验
P0-3 meta job 新增「tag == v + Cargo.toml 版本」断言,不一致即整条流水线失败 .gitea/workflows/release.yml 实测断言逻辑:v1.0.0 通过,v1.0.1 / V1.0.0 拒绝
摘要-4 Release 创建/查询/解析失败一律 exit 1(原为 exit 0) scripts/publish-gitea.sh、.ps1 bash -n 语法检查通过;PowerShell 解析器无错误
README 漂移 订正版本推断说明,并重写「方式二」CI 流程(双平台 → 实际仅 Linux;补 Release/7z OTA 通道与命名约定、repo 写权限要求) README.md 人工核对

批次 0 只解除了"CI 必红"与"版本不可用"两个确定性故障。 让 Linux 客户端真正装上 更新的 P0-6(产物缺可执行位)属于批次 1,尚未实施;OTA 信任模型(P0-4)、 事件总线背压(P0-5)同样未动。

0.5 第二批修复(2026-09-30,批次 1 + 批次 2)

在 0.4 之后继续实施,共 30 个文件、+747/−170。验证:cargo test --lib 34 passed (新增 1 个资产挑选回归测试)、clippy --all-targets 零告警、主题 JSON 解析确认 font.size=14 / radius.lg=10 已生效。

安全与数据完整性

问题 修复
P0-4 7z 路径穿越 新增 verify_archive_entries:解压前只读归档头校验条目清单,必须恰好一个且名字与约定二进制名完全一致(fail-closed);sevenz-rust 0.6.1 的 decompress_file 不做任何路径校验
P0-5 事件总线无背压 通道改有界(容量 1024);emit 分级:高频事件(消息/日志)满即丢并计数、每 1000 条提示一次,低频关键事件(状态/订阅结果/发布回执)异步补投
P0-6 非 Windows 缺可执行位 新增 make_executable(Unix chmod +x,Windows 空实现),暂存产物落盘即补权限
P1-7 校验可静默关闭 ① 无 .sha256 侧车 → 拒绝安装(fail-closed)② install_staged 安装前重算哈希 ③ 同意标记绑定「版本 + 暂存文件哈希」
P1-8 跨卷 rename 失败 新增 place_file:rename 失败回退「复制 + fsync + 删源」
P1-9 备份即删 cleanup_old 保留最近一份 .old 至 OLD_BACKUP_KEEP(7 天),更早的清理
P2-19 无安装互斥 新增 InstallLock(create_new 原子抢占 + 10 分钟陈旧锁接管)
P2-21 死分支 删除永不命中的 .update.tmp 清理条件

正确性

问题 修复
P1-1 协议非法值 端口改 parse_port(1~65535);接收上限/最大报文/主题别名拒 0(parse_opt_positive_u32,0 是 MQTT 5 协议错误);遗嘱主题 trim + 拒绝通配符
P1-2 校验反馈滞后 16 个被校验字段订阅 InputEvent::Change,编辑即清空字段错误
P1-3 重连提示漏字段 session_params_changed 补齐 4 个 v5 CONNECT 属性;测试改为逐字段防漏式断言
P1-4 三元组写死 platform_triple() 改用 std::env::consts::ARCH;连带修复两个写死三元组导致 Linux CI 必红的资产测试,并新增「非约定附件不得选中」测试
P1-5 固定 2s 退避 新增 reconnect_delay:指数退避(1s→30s 封顶)+ 0~500ms 抖动
P1-6 测试连接占住 worker test_handshake 的建连(读证书)放 spawn_blocking,与 connect 一致
P1-10 主题键名不生效 font_size→font.size、radius_lg→radius.lg(实测解析出 14px/10px)
复审 D8 资产挑选退化 pick_assets 按命名约定精确后缀匹配(mqttx[.exe] / .7z),任何含三元组的其它附件不再被当成可执行文件

性能(P1-11 的低成本部分)

问题 修复
每帧深拷贝消息记录 MqttRecord.user_properties 改 Arc<[(String, String)]>(每帧 300 条记录的属性 Vec 深拷贝 → 引用计数)
每帧深拷贝日志 app.logs 改 VecDeque<Arc<LogEntry>>(每帧最多 3000 条 × 4 个 String 的堆分配 → 引用计数)
每行重复读实体 show_millis 提升到 render_messages 帧级,一次读取传入每行

其他(低成本 P2)

P2-5 五个对话框 open() 统一加 has_active_dialog 守卫;P2-6 OptionDelegate::position 按框架契约如实实现;P2-9 阿里云对话框预填即选中(不再复制预设);P2-12 ConnectionConfig / AliyunPreset 手写脱敏 Debug(password/密钥输出 <redacted>, 杜绝一次 {:?} 把凭据送进 Sentry);P2-14 CPU 采样间隔过近时沿用上次结果且不前移 差分基准、修正注释口径;P2-17 非 Windows 不渲染必然报错的置顶按钮; aliyun.rs 补 DeviceName 空值校验;Cargo.toml 补 license = "Apache-2.0"。

仍未修复(明确留待后续)

P1-11 完整版(消息/日志虚拟列表);P2-8 KvEditor 行 uid;P2-11 保存失败不可见; P2-13 硬编码更新源与 Sentry DSN;P2-16 采样在 UI 线程;P2-20 curl 令牌暴露面; 复审 D11(tag 大写 V 的归一化不一致);D12(预发布版本永远收不到正式版); LICENSE 文件本体(元数据已声明 Apache-2.0,正文需从官方源获取); aliyun.rs 一机一密 securemode 与传输层的矛盾(0.5 之外,见 6.4 未决项)。


1. 项目画像

1.1 定位

用 Rust + gpui-kit(GPUI 0.3 / gpui-component 0.6)重写的 MQTT 调试客户端,对标官方 Electron 版 MQTTX。纯 Rust 单二进制,无 Electron/WebView 运行时。早期 Tauri + Vue 实现已归档到 tauri-legacy 分支。

1.2 代码量化

层 行数 文件数 说明
ui/ 7,607 27 占 66%,是所有复杂度的集中地
mqtt/ 1,388 4 引擎:连接生命周期 + v5/v4 事件循环 + TLS
model/ 1,055 8 纯数据层,零 GPUI 依赖
根模块(store/aliyun/sysmon/platform/main/lib) 822 6 持久化、签名、采样、入口
update/ 621 3 OTA
合计 11,493 48 另有 tests 97 行、examples 61 行

1.3 依赖画像

  • 直接依赖 30 项;Cargo.lock 905 个包,其中 89 个包存在多版本(集中在 windows* / hashbrown / getrandom / rand 等传递依赖)。
  • rustls 单版本 0.23.45 —— 这是自定义 TLS(TlsConfiguration::Rustls)能与 rumqttc 内部类型互通的前提,属关键约束。
  • ring 0.17.14 与 aws-lc-rs 1.18.1 同时存在,因此必须在运行时显式安装 CryptoProvider(tls.rs:104-114)——作者已正确处理。
  • 重量级自有选型:sentry(错误上报)、ureq(OTA 下载)、sevenz-rust(纯 Rust 解压)、rfd(原生文件对话框)、embed-resource(图标)。

1.4 产物与构建

项 实测值
target/release/mqttx.exe 20.4 MB(README 写 ~17MB,已过时)
target/debug/mqttx.exe 68.0 MB
release profile lto=true, codegen-units=1, opt-level=3, panic="abort", strip=true
工具链 本机同时装有 stable / nightly;README 声称"必须 nightly",但仓库无 rust-toolchain.toml,CI 用默认 stable 🟡

1.5 提交历史

  • 66 次提交,2026-08-18 → 2026-09-29(41 天),单人双身份(JoeAllen / Joe Allen)。
  • 提交信息质量高,且修复类提交带根因描述(如"连接生命周期竞态与事件处理""数据文件损坏防护、写盘 fsync 与临时文件清理")。
  • 最近两次提交是纯结构重构(拆 model/mqtt/update、拆 ui/app),说明项目正处在**"功能收口、开始整理结构"**的阶段——此刻做架构级修复成本最低。

2. 架构分析

2.1 分层与依赖方向

main.rs ──► ui::app::MqttXApp(唯一状态所有者)
              ├─ model/*      纯数据 + 纯函数(不依赖 GPUI)
              ├─ store.rs     JSON 原子写(tmp + fsync + rename)
              ├─ mqtt/*       引擎(独立 tokio runtime)
              └─ update/*     OTA(ureq + 7z + 自替换)
lib.rs 导出 model/mqtt/store/update,使 tests/ 能脱离 GUI 测引擎

依赖方向严格单向、无环:ui → {model, store, mqtt, update},mqtt → model,model 不依赖任何上层。这是 11k 行规模下最难得的一点:数据层零 GPUI 依赖,33 个单测里有 25 个因此可以直接测业务逻辑。

2.2 并发模型

GPUI 主线程                          engine 线程(tokio, 2 worker)
─────────────                        ──────────────────────────────
MqttXApp 状态                        MqttEngine { conns: Mutex<HashMap>, runtime }
   ▲                                      │
   │ cx.notify()                          ├─ connect(): 占位句柄(client=None) + generation
   │                                      ├─ spawn_blocking: 建 client(读证书/CA 文件)
   │                                      ├─ 事件循环: tokio::select!{ poll() , cancel_rx }
   └── smol::channel::unbounded ◄─────────┘  emit(): tx.try_send(event)   ← 无界、不阻塞

三个关键设计:

  1. 连接代数(generation):同一 id 的每次 connect 递增代数;事件循环退出时只删自己那一代(remove_handle_if)。这修掉了"重连后句柄被旧循环误删、从此无法从 UI 断开"的经典竞态。
  2. 建连期占位句柄 client: None:让 close() 在建连中途也能取消,且 is_connected() 在此之前恒为 false,天然做了防抖。
  3. 阻塞 IO 分离:connect 把证书读取放进 spawn_blocking(mqtt/mod.rs:198-200),OTA 下载走 run_blocking。但 test_connection 是例外(见 P1-6)。

缺口:emit 用 try_send(mqtt/mod.rs:127-128),通道容量由 UI 侧决定(ui/app/mod.rs:129 用的是 unbounded)。当前等价于"永不丢弃、永不阻塞"——见 P0-5。

2.3 一次消息的完整旅程

broker ──► eventloop.poll() ──► v5_record() ──► engine.intern(topic)   ← Arc<str> 驻留
                              │                  engine.emit(Message)
                              ▼
                     [无界 smol 通道]  ← 背压缺口在这里
                              ▼
   UI pump ──► on_engine_event():
        ├─ 丢弃已删除连接的事件(防泄漏)
        ├─ push_message()  → MessageRing(条数 + 32MB 字节双预算驱逐)
        ├─ push_log()      → 内存环形(3000) + 同步写 mqttx-YYYY-MM-DD.log  ← 磁盘 IO 在 UI 线程
        └─ view.notify()   → 下一次渲染重建 ≤300 行消息

这条链路有三处刻意优化(主题驻留、双预算驱逐、日志句柄按日缓存),也有三处代价(无背压、UI 线程落盘、每帧全量重建),构成性能与可靠性问题的主干。

2.4 连接生命周期

ConnectionStatus 四态由引擎单向推送,UI 不猜测状态,只在 Connected 事件上触发 auto resubscribe。订阅采用乐观更新:先入库落盘,SUBACK 失败再回滚,并特意避免"SUBACK 前被删除的订阅被事件用默认字段复活"。

2.5 持久化

  • 数据目录:%APPDATA%\MQTTX-GPUI(刻意与官方 Electron 版 MQTTX/ 区分)。
  • 6 个 JSON 文件:connections / subscriptions / presets / variables / aliyun / settings。
  • 写入语义:tmp(pid 命名) → write_all → fsync → rename,失败清理 tmp。
  • 损坏文件防护:加载失败的文件名进 session 集合,下次保存前先重命名为 .corrupt-<时间戳> 备份,把"连接全没了且被空数据覆盖"从不可恢复降为可抢救。
  • 读取容忍 UTF-8 BOM。

2.6 OTA 链路

启动 ──► cleanup_old() ──► load_staged() ──► consent 匹配? ──► install_staged()(改名+spawn 新进程)──► 退出
                                  │ 否
                                  └─► 进入 UI,auto_check_update 时联网检查
check_latest() ─► pick_assets() ─► download_and_stage() ─► 大小对账 + sha256 ─► 弹窗征得同意

设计上明确"下载即暂存、用户同意才安装",拒绝静默强制升级——产品方向正确;代价是暂存区成为长期驻留的攻击面(见 P0-4、P1-7)。

2.7 UI 组织与渲染

ui/app/(外壳与状态)+ ui/connection_view/(单连接工作区)+ ui/dialogs/(模态)+ ui/widgets/(复用件)。ConnectionView 是最大的实体(约 30 个字段,含 12 个 Entity<InputState>)。

渲染上限:MAX_RENDERED_MESSAGES = 300、MAX_RENDERED_LOGS = 3000;消息最新在最上方。每帧全量重建,无虚拟列表——见 P1-11。


3. 模块点评

模块 职责 亮点 主要问题
model/ 纯数据 + 纯函数 字节预算环形缓冲;非文本负载保留原始字节;模板渲染单时间基准 session_params_changed 漏字段(P1-3)
mqtt/ 引擎与事件循环 连接代数、占位句柄、可取消退避、显式 CryptoProvider 无背压(P0-5)、test_connection 阻塞 IO(P1-6)、固定 2s 退避(P1-5)
store.rs JSON 原子持久化 fsync+rename、损坏备份、BOM 容忍 明文密码落盘(P2-12)
update/ OTA 大小对账、双阶段回滚、同意绑定版本 路径穿越(P0-4)、缺可执行位(P0-6)、TOCTOU(P1-7)、跨卷 rename(P1-8)、备份即删(P1-9)
aliyun.rs 阿里云签名 Token 模式与官方一致 securemode/传输矛盾(未决-2)、无测试
sysmon.rs 进程采样 Win32 结构口径正确、失败不 panic Linux 测试必失败(P0-1)、CPU 基准前移(P2-14)
platform.rs 窗口置顶 失败路径不 panic 非 Windows 必报错(P2-17)、双分派死代码
ui/ 全部界面 色值零硬编码、异步回调统一弱引用 渲染热路径(P1-11)、对话框栈语义(P2-5)、键名不生效(P1-10)

4. 值得肯定的设计

  1. 连接代数 + 占位句柄(mqtt/mod.rs:153-250)——正确解决"重连/关闭与建连并发"这一最难调的竞态。
  2. 消息环形缓冲的条数 + 字节双预算(message.rs:136-146,32MB/连接)——大报文流下条数上限形同虚设(2000×128KB≈256MB),作者显式解决了。
  3. 非文本负载保留原始字节(message.rs:180-197)——lossy 文本会失真,Hex/Base64 详情仍能对上线上报文。
  4. 损坏文件"先备份再覆盖"(store.rs:88-103)——把不可恢复的丢失降为可抢救。
  5. 落盘 tmp+fsync+rename + pid 命名(store.rs:105-122)——掉电语义正确,并发不互踩。
  6. 显式安装 rustls CryptoProvider(tls.rs:104-114)——ring 与 aws-lc-rs 并存时 ClientConfig::builder() 会 panic,属隐藏地雷,被提前拆掉。
  7. 下载大小对账(install.rs:110-116)——弥补 read()==0 无法区分"正常结束/断流",判断精准。
  8. 自替换双阶段回滚(install.rs:212-242)——rename 失败与 spawn 失败都尝试回滚,并区分"已恢复/回滚失败+备份路径"。
  9. 同意标记绑定版本(install.rs:265-271)——避免"下完就换"。
  10. 主题色值零硬编码——范围内 11 个文件仅 3 处出现字面色,且都是主题色的 alpha 派生,浅/深色都成立。
  11. 异步回调统一弱引用 + .ok()——无 Entity 泄漏,不会在实体/窗口销毁后 panic;资源监控用弱引用退出循环并有注释说明。
  12. 消息内存按字节预算与 flood 压测工具并存,说明作者主动验证过内存行为。
  13. sysmon 的 Win32 用法正确——PROCESS_MEMORY_COUNTERS_EX 扁平结构 + 按 EX 大小传 cb,PrivateUsage 取"提交内存"口径;CPU 用 GetProcessTimes 差分而非近似采样。
  14. build.rs 用 CARGO_CFG_TARGET_OS 而非宿主 OS——交叉编译时行为正确;"资源 ID 1"的注释与 gpui-pre-windows 的 LoadImageW 实测一致。

5. 问题清单

每条格式:现象 → 证据(文件:行)→ 影响 → 修复方向。标题后的标记表示验证状态。文件路径相对仓库根,格式为 路径:行。

P0-1 ✅实测 【阻断】Linux 单测确定性失败,CI 永远红 —— 🛠 已修复(见 0.4)

  • 证据:sysmon.rs:9(Sample 派生 Default)、41(非 Windows 直接返回 Sample::default(),working_set = 0)、194(无条件 assert!(second.working_set > 0));release.yml:39-46 在 ubuntu-latest 跑 cargo test -p mqttx-desktop --lib;release.yml:52-53 build/publish needs: [meta, test]。
  • 加重因素:test job 未安装任何图形库 -dev 包,而同一 workflow 的 build job 装了 10 个("Install Linux system deps (GPUI)")。同一份依赖图,test job 在干净 runner 上大概率连编译都过不去。
  • 影响:打 v* tag 后 Test 阶段即断,产物与 Release 一个都发不出去。该红灯在 sysmon 引入之后才存在,而最后一次 tag(v1.0.0)早于它,因此从未暴露。
  • 修复:该测试加 #[cfg(target_os = "windows")] 或分平台断言;test job 复用 build job 的依赖安装步骤。

P0-2 ✅实测 【OTA】发布脚本产出非 semver 版本,静默废掉更新通道 —— 🛠 已修复(见 0.4)

  • 证据:publish-gitea.sh:71-78、publish-gitea.ps1:115-123 用 git describe --tags --always 兜底版本。实测本仓库 HEAD:git describe --tags --always → v1.0.0-4-gbc9f55c,--exact-match 失败(HEAD 无 tag)。
  • 影响链:VER_NUM = 1.0.0-4-gbc9f55c → 创建/复用该 tag 的公开 Release → 客户端 release_part(update/mod.rs:78-84)截成 1.0.0 → is_newer 判否 → 老用户永远收不到更新;且该 Release 会被 /releases/latest 命中,把真正的更新挡在后面。
  • 修复:脚本入口强制 semver 正则校验,不匹配即失败;把 --always 换成 --exact-match。

P0-3 ✅代码读取 【发布一致性】版本双真源,无一致性校验 —— 🛠 已修复(见 0.4)

  • 证据:Cargo.toml:3(CARGO_PKG_VERSION,当前 1.0.0)vs release.yml:33(GITHUB_REF_NAME),workflow 内无比对步骤。
  • 影响:打 v1.0.4 但忘 bump → 新包装完仍自报 1.0.0 → 每次启动重复提示同一更新(无限循环);反向则永远无更新提示。
  • 修复:CI 增加 v$(Cargo.toml version) == tag 断言;或让 build.rs 从 tag 生成版本,收敛为单一真源。

P0-4 ✅代码读取 【安全】7z 解压路径穿越 + 同源 sha256

  • 证据:install.rs:59 调用 sevenz_rust::decompress_file;依赖 sevenz-rust-0.6.1/src/de_funcs.rs:102 直接 dest.join(entry.name()),不校验绝对路径与 ..,且 default_entry_extract_fn 会自动 create_dir_all 父目录。校验值来自同一 Release 的侧车(check.rs:52-59 → install.rs:119)。
  • 影响:能创建 Release 的人(或能改写 Gitea 响应的一方)可对所有客户端完成任意路径写入(含启动目录),随后 install_staged 还会执行解压出的同名二进制。sha256 只防传输损坏,不防伪造。
  • 修复:改用 decompress_with_extract_fn 做白名单 + 拒绝 ../盘符/UNC + 限制条目数与解压总量;同时引入 ed25519/minisign 签名(内嵌公钥)作为真正的信任锚。

P0-5 ✅代码读取 【可靠性】事件总线无背压,极端流量下内存无上界

  • 证据:ui/app/mod.rs:129 建无界通道;mqtt/mod.rs:127-128 try_send 永不失败;消费端每条事件做线性查找 + 入环形缓冲 + 同步写日志文件(events.rs:184 → 195-259)+ cx.notify()。
  • 影响:环形缓冲只约束"已入库"的记录,不约束"在途"事件。到达速率持续超过 UI 消费速率时队列无界增长,每个在途 Message 事件最长可携带 128KB payload。仓库自带的 examples/flood.rs 正是按速率压测的发布器,说明作者关心该场景,但管道本身无兜底。
  • 修复:改有界通道 + 显式丢弃策略(消息可丢、状态/日志不可丢);或把 Message 事件批量化(一次携带 Vec<MqttRecord>),把"每条一次 notify"变成"每帧一次";日志落盘移出热路径。

P0-6 ✅代码读取 【OTA】非 Windows 自更新因缺可执行位必定失败

  • 证据:install.rs:92 用 File::create 落盘(Unix 权限 0644),7z 路径的解压同样走 File::create;install.rs:212 rename 就位(保留权限);install.rs:220 Command::new(&cur).spawn() → EACCES,走回滚 222-227。
  • 影响:Linux/macOS 用户永远装不上,且提示只有一句"启动新版本失败"。
  • 修复:Unix 下在 rename 前 set_permissions(0o755)。

P1-1 ✅代码读取 【协议】MQTT 5 的 0 值参数直接进 CONNECT 并落盘

  • 证据:connection_form.rs:431-444 的 parse_opt_u32 对 "0" 返回 Some(0)(只做 u16 上限转换)→ v5.rs:79-85 原样 set_receive_maximum(Some(0)) / set_max_packet_size(Some(0)) → 保存即写 connections.json。
  • 影响:MQTT 5 规范把 Receive Maximum = 0 与 Maximum Packet Size = 0 定义为 Protocol Error,broker 直接断连;值被持久化,重启后持续失败,用户只看到"连接测试失败"。
  • 修复:解析后 filter(|v| *v > 0),文案写明"必须 ≥1(0 为协议错误)"。

P1-2 ✅代码读取 【校验】表单校验"一次性 + 首错即停 + 无变更订阅"

  • 证据:connection_form.rs:387-389(每次 build 先 clear)、346-350(fail 后立即 ? 返回)、731-734(仅从 field_errors 渲染,无输入变更订阅)。
  • 影响:改好端口后红字仍在;3 个字段非法要点 3 次保存才能看全。
  • 修复:订阅 InputState 的 Change 清除对应 key;或一次性收集全部错误。

P1-3 ✅代码读取 【正确性】session_params_changed 漏掉 4 个 v5 CONNECT 属性

  • 证据:connection.rs:255-268 比较了 host/port/path/transport/protocol/client_id/账号/clean_start/keep_alive/遗嘱/SSL,唯独漏了 session_expiry_interval、receive_maximum、maximum_packet_size、topic_alias_maximum——而它们在 v5.rs:73-87 确实是随 CONNECT 一次性生效的。
  • 影响:连接已建立时改这四项,用户不会收到"重连后生效"提示,改动静默不生效。对应的单测自称覆盖"所有 CONNECT 时报文生效字段",实际未覆盖,给了虚假安全感。
  • 修复:补齐比较;测试改为遍历式断言(列出全部 CONNECT 字段逐一改动验证)。

P1-4 ✅代码读取 【平台】platform_triple() 硬编码 x86_64

  • 证据:update/mod.rs:56-64 只按 cfg!(target_os) 判断,架构写死 x86_64-*;而发布脚本 publish-gitea.sh:107-108 明确支持 aarch64-apple-darwin / aarch64-unknown-linux-gnu。
  • 影响:ARM 客户端永远发现不了更新;发布端若产出 aarch64 资产,则成为永远无人下载的僵尸资产。当前 CI 只出 Linux x86_64,属潜在坑。
  • 修复:std::env::consts::ARCH 参与拼接;补"客户端三元组 == 脚本三元组"的对照测试。

P1-5 ✅代码读取 【可靠性】重连固定 2 秒,无指数退避与抖动

  • 证据:v5.rs:338-341、v4.rs:301-304;max_reconnect_times 默认 0 即无限重连。
  • 影响:broker 长时间不可用时每 2 秒一次尝试 + 每 2 秒一次 Status::Error 事件 → UI 抖动与日志刷屏;多连接会在同一时刻齐步重连(惊群)。
  • 修复:指数退避(1s→2s→…→60s)+ 抖动;连续失败期间不重复推送 Error 状态。

P1-6 ✅代码读取 【一致性】test_connection 在 runtime 线程做阻塞 IO

  • 证据:mqtt/mod.rs:568-585 直接把 test_handshake(&cfg) 放进 timeout,而 test_handshake 内部同步调用 build_v5/build_v4(读 CA/证书文件)。对照 mqtt/mod.rs:198-200,作者在 connect 里明确写了"build 含阻塞 IO,放 spawn_blocking"。
  • 影响:慢盘上测试连接会占住 2 个 worker 之一,影响同一 runtime 上其他连接的收发。
  • 修复:同样走 spawn_blocking。

P1-7 ✅代码读取 【安全】安装前不复核 + 无侧车静默跳过校验

  • 证据:install.rs:204-231 只 rename+spawn,不重算哈希;check.rs:115-119 同版本已暂存即跳过下载;install.rs:177-200 信任目录内任何命名合规文件;install.rs:265-271 consent 只比对版本字符串。侧车缺失时 check.rs:52-59 返回 None → install.rs:153-156 → 跳过校验,而 UI 仍写"已通过 sha256 校验"(ui/app/mod.rs:173)。
  • 影响:一次 Release 漏传侧车,所有客户端就在零完整性校验下安装;下载校验完成到"下次启动安装"之间,暂存文件被替换/损坏仍会被原样安装。
  • 修复:无侧车拒绝安装或显式降级提示;安装前重算哈希;consent 绑定哈希。

P1-8 ✅代码读取 【可靠性】跨卷 rename 必失败,且报错误导

  • 证据:install.rs:11-18 暂存在 %APPDATA%\MQTTX-GPUI\updates;install.rs:205-212 把 exe 同目录的文件改名就位。Windows 上跨卷 MoveFile 直接失败(ERROR_NOT_SAME_DEVICE),而错误文案是"替换失败(可能无写入权限)"。
  • 影响:应用装在 D 盘、%APPDATA% 在 C 盘时更新必失败;域环境 Roaming 重定向到网络盘更常见。
  • 修复:rename 失败回退 copy + fsync + replace;或把暂存放 exe 同目录。

P1-9 ✅代码读取 【可靠性】新版本首启即删除唯一回滚备份

  • 证据:main.rs:26(安装后新进程启动第一步就 cleanup_old())→ install.rs:289-293 无条件删 *.old;ui/app/mod.rs:144 再删一次。
  • 影响:自替换已覆盖原 exe,备份又被新版本自己删掉;新版本一旦启动即崩溃,用户没有任何自助回退路径。
  • 修复:新版本健康运行一段时间后再删;或保留最近 1-2 份 + 启动失败自动回退。

P1-10 ✅代码读取 【UI】主题 font_size / radius_lg 键名不符规范,被静默忽略

  • 证据:linear-theme.json:6-8 写 "font_size": 14 / "radius": 6 / "radius_lg": 10;依赖 schema gpui-component-0.6.6/src/theme/schema.rs:36-71 为 #[serde(default)] 且 rename = "font.size"(默认 16)、rename = "radius.lg"(默认 8)、rename = "radius"(默认 6),未知键不报错。
  • 影响:主题里写的 14px 字号与 10px 大圆角从未生效(实际是框架默认 16px / 8px),只有 radius: 6 因为同名而生效。README 的"14px 紧凑排版"因此不成立。
  • 修复:改成 "font.size": 14、"radius.lg": 10;建议补一条"主题键名与 schema 对齐"的测试。

P1-11 ✅代码读取 【性能】渲染热路径全量深拷贝

  • 证据:消息每帧 r.clone() 最多 300 条(messages.rs:438-478),MqttRecord::clone 虽共享 Arc<str>,但 user_properties: Vec<(String,String)> 与 4 个 Option<String> 是深拷贝;日志每帧 filter(..).take(3000).cloned()(messages.rs:687-698),每条 LogEntry 含 4 个 String;发布面板每帧对 topic+payload 做 extract_placeholder_keys(publish.rs:180-184),展开时还每帧 render_template(vars.rs:84-88);每行额外一次 with_app 读 show_millis(messages.rs:153-155)。
  • 影响:日志面板打开且有 3000 条日志时,60fps 下约 18 万次 LogEntry 克隆/秒(≈72 万次 String 分配/秒);发布大负载时每帧一次 MB 级拷贝。
  • 修复:虚拟列表(只渲染可见窗口);show_millis/subs 提到帧级只算一次;用 Arc<MqttRecord> 让列表渲染零拷贝;占位符提取改为 Change 事件增量计算并缓存。

P2 清单(22 项,摘要)

# 问题 证据 影响
P2-1 端口允许 0 connection_form.rs:361-365、405 非法值落盘,报错与真实原因无关
P2-2 遗嘱主题不 trim、不校验通配符 connection_form.rs:530-546 保存成功、连接必失败
P2-3 传输切换的"默认端口"判定跨所有传输 connection_form.rs:257-267 手输 8084 后切 TLS 被静默改 8883
P2-4 非 WS 传输下 path 仍入库但输入框隐藏 connection_form.rs:500-503 隐形脏数据
P2-5 close_dialog 关栈顶而非自己;叠加守卫只挂 5 个快捷键 settings_dialog.rs:568、ui/app/mod.rs:284-330 嵌套确认时可能关错层
P2-6 OptionDelegate::position() 恒 None,违背框架契约 widgets/select.rs:49-59 一旦启用搜索或 set_selected_value 即"下拉显示空、保存写默认值"
P2-7 KvEditor 空 key 静默丢弃、重复 key 静默覆盖 widgets/kv_editor.rs:54-73 用户填的行保存后无声消失
P2-8 KvEditor 用下标做元素 id widgets/kv_editor.rs:85-100 删中间行后 hover/焦点串台一帧
P2-9 阿里云对话框预填 first 但 selected=None dialogs/aliyun_dialog.rs:54-58、188-190 打开即保存 → 凭空复制一条预设
P2-10 阿里云"生成并连接"每次新建连接并落盘 dialogs/aliyun_dialog.rs:206-213、ui/app/actions.rs:16-23 连点两次多两条同名连接
P2-11 保存失败不可见(写盘失败仅 elog) ui/app/actions.rs:16-23、store.rs:140-144 磁盘满/无权限时用户以为已保存
P2-12 明文落盘与导出:连接密码、AccessKeySecret/DeviceSecret store.rs:221-225、aliyun.rs:66-77 导出文件即凭据;两结构体 derive(Debug),一次 {:?} 就进 stderr+Sentry(当前未见此类调用)
P2-13 硬编码基础设施地址与 Sentry DSN main.rs:8-9、update/mod.rs:22-24 换源/开源需改代码
P2-14 CPU 采样 dt≤50ms 时既报 0 又前移基准;注释与公式矛盾 sysmon.rs:118-125、17 手点"立即刷新"读到 0.0%;按注释理解会误判 8 倍
P2-15 非 Windows 返回"全 0"而非"能力缺失" sysmon.rs:41-43 UI 显示 0 B / 0%,无法区分"不支持"
P2-16 线程采样抓全系统快照并同步跑在 UI 线程 sysmon.rs:131-148 线程数上万时毫秒级开销落在渲染路径
P2-17 非 Windows 置顶按钮必弹错误;双分派死代码 platform.rs:5-8、37-40 跨平台产品上的坏按钮
P2-18 build.rs 用 manifest_required() 把"缺 rc.exe"升级为硬失败 ⚠️ build.rs:11-13 无 Windows SDK 时连 UI 都起不来(代价只是图标回退默认)
P2-19 无单实例/安装互斥 全仓库无单实例锁 两实例互删暂存、争抢同一个 .old
P2-20 发布脚本令牌走 curl 命令行;Release 失败 exit 0 publish-gitea.sh:119-139、174-181 同机 ps 可见令牌;job 变绿但 OTA 通道为空
P2-21 .update.tmp 清理分支永不命中 install.rs:290 vs 39-47 历史遗留 tmp 永远清不掉
P2-22 无 LICENSE 却宣称 Apache-2.0;Cargo 无 license 字段 settings_dialog.rs:506;glob LICENSE* 为空 对外分发前的合规缺口

其他低危项(供参考):engine_live.rs 从不读取 MQTTX_LIVE 环境变量;pump() 把通道关闭当空轮询;flood.rs 的 eventloop 静默吞错且统计不区分"已请求/已发出";widgets/select.rs:84 的 select_element 零调用;logo.rs:122 死导入;resource_dialog 连接卡缺溢出保护。


6. 专项分析

6.1 发布链路:三种走法都不通(推演)

走法 结果 依据
CI 自动发布(tag → build → publish) 不通:Test 阶段必断 P0-1
本地脚本默认路径(不带 -Version 直接跑) 通路但产出垃圾版本:Release tag 变成 v1.0.0-4-gbc9f55c P0-2(实测)
本地脚本显式版本(-Version v1.0.1) 相对可用,但仍需人工保证 tag 与 Cargo 版本一致,且 Windows triple 硬编码 P0-3、P2-20

结论:在批次 0 完成之前,"发布"这件事在三种走法里没有一条是可靠的。这也解释了为什么 OTA 相关缺陷至今未被触发——通道本身从未真正跑通过。

6.2 OTA 信任模型

当前信任锚只有两个:HTTPS/TLS 证书 与 Gitea 的发布权限。完整性校验(sha256 侧车)与产物同源,只能证明"传输没坏"。叠加以下事实,构成一条完整的自动执行链:

  1. 暂存目录与同意标记都在用户可写的 %APPDATA%(P1-7 / P2-19);
  2. load_staged 信任目录内任何命名合规的文件(install.rs:177-200);
  3. 同意标记是纯文本版本号,任何同用户进程可伪造(install.rs:246-271);
  4. 安装前不复核哈希(P1-7);
  5. 7z 解压可越界写(P0-4)。

当应用安装在 Program Files(替换 exe 需管理员)时,这等于把用户可写目录接进了管理员执行路径。

加固优先级:签名验证 > 解压白名单 > consent 绑定哈希 > 安装前复核 > 安装互斥锁。

6.3 性能与内存

  • 已做对的:单条 payload 128KB 截断、每连接 32MB 字节预算、主题字符串驻留、消息列表渲染上限 300。
  • 缺口:无背压(P0-5)、每帧全量深拷贝(P1-11)、日志同步落盘。
  • 失效模式链:高频消息 → UI 线程写日志/重建元素变慢 → 无界队列积压 → 内存增长。三处缺一不可,先修背压收益最大。
  • 建议观测指标:单连接 payload+raw 驻留字节(已有 retained_bytes() 可直接复用)、通道在途事件数、单帧渲染耗时。

6.4 阿里云签名争议 ❌ 未采信

争议点:一机一密的 signing content 中 clientId 段应取什么值。

复审意见(被列为中高缺陷):认为官方 content 应使用"客户端实际发送的完整 clientId",且 clientId 前缀应为 deviceName 而非 productKey.deviceName。

本报告不采信,理由:

  1. 实现、模块文档、提交信息三者自洽——aliyun.rs:11-13 明写 content 为 clientId{ProductKey}.{DeviceName}deviceName{...}productKey{...}timestamp{...},代码 176-179 完全照此实现;提交 2f35bca("按物联网平台官方直连规范重写")的 diff 显示这是对照规范后的重写结果,不是随手写的。
  2. 该拼装形式(content 的 clientId 段取 pk.dn、不带 |securemode…| 后缀)与常见 Aliyun IoT SDK 示例一致。
  3. 复审自身声明"无法联网核对官方原文",其论据"两处拼装必须自洽"并不成立——规范允许 content 中的占位值与报文中的字面值不同。
  4. 本报告作者尝试核验亦失败:本环境 help.aliyun.com 解析到非公网 IP(web_fetch 直接拒绝),web_search 超时。

处理建议(发布前必做,约 10 分钟):用官方文档自带的示例四元组(ProductKey / DeviceName / DeviceSecret / timestamp → 示例 password)建立离线测试向量,两种拼装各算一次,与示例比对。这同时补上了 aliyun.rs 目前零测试的缺口。

但"securemode 与传输层矛盾"保留为未决项(见附录 C):代码在 clientId 里声明 securemode=2(官方语义为 TLS 直连),而 aliyun.rs:142-143 固定 TransportKind::Tcp(1883)。这是代码内部可判定的不一致,与 content 规则如何无关——声明与实际传输必须一致(要么 securemode=3 + TCP,要么 TLS + 8883)。需设备实测确认 broker 是否拒绝。

6.5 主题与视觉

  • 做得好的:色值零硬编码,全部经 cx.theme() 或主题色 alpha 派生;hue_color() 按色相段微调明度以兼顾浅/深色;订阅色板有语义。
  • 缺陷:P1-10 的两个键名被静默忽略,导致"14px 紧凑排版"名不副实。
  • 建议:把主题 JSON 的键名与 schema.rs 做一次对齐检查并纳入 CI(该 JSON 是 include_str! 内嵌的,编译期无法发现键名错误)。

7. 测试与质量保障

7.1 现状

项 值
单测数量 33 个,全部通过(本机 Windows,0.19s)
分布 model/mod.rs 18、update/mod.rs 6、store.rs 5、sysmon.rs 2、connection_view/util.rs 2
集成测试 tests/engine_live.rs 2 个,默认 #[ignore],依赖外网
无测试的源文件 43 / 48
静态检查 cargo clippy --all-targets 零告警
CI 覆盖 cargo test --lib(Linux,当前必失败)+ 默认 cargo build;不编译 tests/examples

7.2 覆盖矩阵

模块 单测 说明
model(纯函数) ✅ 充分 覆盖了 Hex 奇数长度 panic、UTF-8 截断边界、字节预算、向后兼容反序列化等真实缺陷
store ✅ 基本够 导入导出往返、BOM、损坏备份
update(纯函数) ⚠️ 偏弱 版本比较/资产挑选有覆盖;签名、解压、安装路径三条最危险的分支零覆盖;"命名约定"测试是同义反复(把同一个 format! 再写一遍),无法发现与 CI/脚本的偏离
mqtt(引擎) ❌ 零 并发、生命周期、代数、取消语义全无自动化验证
aliyun ❌ 零 签名是纯函数,最适合表驱动测试
ui ❌ 仅 util 表单校验、对话框生命周期无覆盖

根本问题不是测试写得差,而是模块选错了:测试全押在纯数据层,把风险最高的引擎留给了手工验证。

7.3 建议补的最小测试集(均无需外网)

  1. 连接代数与 remove_handle_if 语义、close 期间建连完成的丢弃路径;
  2. max_reconnect_times 计数与成功后清零;
  3. topic_matches 的 MQTT 规范边界(a/# vs a、+ vs 空层、sport/+ vs sport/);
  4. session_params_changed 的字段完备性(遍历式防漏);
  5. 阿里云两种签名对官方测试向量;
  6. connection_form::build 的边界(端口 0 / 65536、receive_maximum 0、遗嘱主题带 #);
  7. 主题 JSON 键名与 schema.rs 对齐。

8. README 与实现不一致清单

# README 实际
1 L13 "用户属性…连接/遗嘱/发布三处均支持" 只有发布支持。ConnectionConfig 无该字段;LastWill 只有 content_type/response_topic
2 L160 "Windows / Linux 双平台 release 构建" release.yml:62-65 Windows 矩阵整段被注释,CI 只出 Linux
3 L160 命名 mqttx-<ver>-<target>-<ext> 实际是 mqttx-<ver>-<target>-mqttx[.exe](<ext> 易被误读)
4 L161 只写"上传到包注册表" 完全漏掉 Release 附件(7z + 裸二进制 + .sha256)这一 OTA 唯一通道与命名约定
5 L99 "前置:Rust nightly" 无 rust-toolchain.toml;CI 用默认 stable;代码无 #![feature] 🟡
6 L118 MQTTX_LIVE=1 代码从不读该变量,只有 #[ignore] 生效
7 L132 只需 package 写权限 脚本还要创建 Release/上传附件,需 repo 写权限
8 L6 归档链接 ../../archive/tauri-legacy.tar.gz 不存在;真实归档是分支 tauri-legacy
9 L24 快捷键列表 缺 Ctrl+Shift+R(资源监控);Ctrl+Enter 只在主题/负载有焦点时生效
10 L26 设置项列表 漏了手动检查更新/下载进度
11 L29-37 技术栈表 漏 sentry(全文未提会把 panic 上报到自建 Sentry,且该功能无开关)、ureq/sha2/sevenz-rust/rfd/rustls/smol 等
12 L43-95 目录树 漏 build.rs、resources/、examples/flood.rs、linear-theme.json、scripts/、.cargo/config.toml、.gitea/
13 L178 "release ~17MB" 实测 20.4 MB
14 功能特性 完全未提 OTA 自更新与资源监控两个子系统;关于页宣称 Apache-2.0 但仓库无 LICENSE
15 — 已核对一致的部分:数据目录表、BOM 容忍、产物路径、Ctrl+N / Ctrl+, / Ctrl+Shift+E / Ctrl+Shift+I、重连与超时字段、订阅 v5 选项、分组 chips、.cargo/config.toml 镜像说明

9. 修复路线图

批次 0 —— 解阻发布(约半天,最高 ROI)

  1. sysmon 测试加 #[cfg(target_os = "windows")] 或分平台断言;test job 复用 build job 的系统依赖安装,并核实 libasound2-dev 是否多余(Cargo.lock 中无 alsa/cpal 相关包)。
  2. 发布脚本入口强制 semver 正则校验 + --exact-match,不匹配即失败。
  3. CI 增加 tag == Cargo.toml version 断言。
  4. Release 创建/上传失败一律 exit 1。
  5. 验收:本地 cargo test --lib 在 Linux 容器内通过;打一个测试 tag,CI 全绿并产出 Release 资产;客户端能发现该版本。

批次 1 —— 安全与数据完整性(1~2 天)

  1. 7z 解压改 decompress_with_extract_fn:白名单文件名、拒绝 ../盘符/UNC、限制条目数与总量;引入 ed25519 签名验证。
  2. Unix 下 set_permissions(0o755)。
  3. 无侧车不静默跳过;安装前复核哈希;consent 绑定哈希;安装互斥锁。
  4. 事件总线改有界 + 显式丢弃策略;日志落盘移出事件热路径(批量/异步)。
  5. 跨卷 rename 回退 copy+fsync+replace;.old 保留至新版本健康启动。
  6. 验收:在 Linux 上完整走通一次自更新;构造一个带 .. 条目的 7z 验证被拒绝;构造洪水流量观察内存曲线有上界。

批次 2 —— 正确性与边界(1~2 天)

  1. Receive Maximum/Maximum Packet Size 拒 0;端口 1..=65535;遗嘱主题 trim + 通配符校验。
  2. session_params_changed 补 4 字段并改成防漏式测试。
  3. platform_triple 用 std::env::consts::ARCH。
  4. test_connection 补 spawn_blocking;重连指数退避 + 抖动。
  5. 主题 JSON 键改 "font.size" / "radius.lg"。
  6. 验收:新增单测全绿;CI 加 cargo clippy -D warnings 与 cargo test --lib --tests(#[ignore] 只跳执行、仍会编译)。

批次 3 —— 体验与工程化(2~3 天)

  1. 消息/日志列表虚拟化 + 帧级缓存。
  2. 对话框栈守卫下沉到各 dialogs::*::open;close_dialog 语义明确化。
  3. 实现 OptionDelegate::position;KvEditor 空/重复 key 显式提示。
  4. 阿里云对话框语义对齐(预填即选中、按预设复用连接)。
  5. 补 LICENSE 与 Cargo 元数据;README 按第 8 节逐条订正。

10. 附录

附录 A:方法与验证记录

动作 结果
cargo test -p mqttx-desktop --lib 33 passed / 0 failed(0.19s,Windows)
cargo clippy -p mqttx-desktop --all-targets 零告警
git status 干净(仅 .zcodeignore 未跟踪);本报告为唯一新增文件
git describe --tags --always v1.0.0-4-gbc9f55c(仓库唯一 tag 为 v1.0.0)
依赖核对 直接读取 Cargo.lock(905 包 / 89 个多版本)与 cargo registry 源码(sevenz-rust-0.6.1、gpui-component-0.6.6、embed-resource、ureq-2.12.1)
未完成的核验 阿里云官方文档(本环境 help.aliyun.com 解析到非公网 IP,web_fetch 被拒;web_search 超时)

附录 B:可信度台账

结论 状态
Linux 单测必失败、git describe 产出垃圾版本、非 Windows 缺 +x、主题键名不生效、阿里云对话框空保存复制预设、7z 路径穿越 ✅ 实测 / 代码读取
ureq 默认 https_only=false 且跟随重定向;manifest_required() 的失败语义 ⚠️ 依赖源码读取,未做实验
阿里云 content 拼装"错误" ❌ 未采信(见 6.4)
rc.exe 缺失导致构建失败、Windows 跨卷 rename 失败 🟡 语义明确但未在目标环境复现
MQTT 5 参数语义(Receive Maximum / Maximum Packet Size 为 0 即 Protocol Error) ✅ 规范明确 + 代码路径已核对

附录 C:未决 / 需实测问题

  1. 阿里云一机一密 signing content:需官方测试向量离线比对(见 6.4)。
  2. securemode=2 + 明文 TCP:1883 是否被 broker 拒绝:需真实设备实测;无论结果如何,"声明与传输必须一致"这一条成立。
  3. stable 工具链能否构建:本机只用 nightly 验证过;CI 用 stable,需一次干净环境实测。
  4. libasound2-dev 是否必要:Cargo.lock 中无 alsa/cpal/libpulse/jack,倾向于多余,但需在干净 runner 上验证。
  5. 发布脚本 git credential fill 回退路径的实际作用域:取决于本机 GCM 配置,需按最小权限原则收窄。

附录 D:关键文件索引

关注点 文件
引擎与生命周期 gpui-app/src/mqtt/mod.rs、v5.rs、v4.rs、tls.rs
数据模型 gpui-app/src/model/*.rs
持久化 gpui-app/src/store.rs
OTA gpui-app/src/update/{mod,check,install}.rs
阿里云 gpui-app/src/aliyun.rs
UI 外壳与状态 gpui-app/src/ui/app/*.rs
连接工作区 gpui-app/src/ui/connection_view/*.rs
表单与对话框 gpui-app/src/ui/connection_form.rs、gpui-app/src/ui/dialogs/*.rs
发布与 CI .gitea/workflows/release.yml、scripts/publish-gitea.{sh,ps1}
测试与压测 gpui-app/tests/engine_live.rs、gpui-app/examples/flood.rs

本报告基于 HEAD bc9f55c 的只读分析,未对仓库源文件做任何修改。