Files
NixMsg/docs/DEVIATIONS.md
T

244 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现与文档的偏差
开发中凡是实现和 [PRD.md](./PRD.md)、[DEVELOPMENT.md](./DEVELOPMENT.md) 不一致的地方,以及文档没写清、开发中自己拿主意的地方,都记在这里,交给负责人事后审阅。开发全程自动推进,记下来就继续,不等审阅。
每条写清:日期、原条款(文档和小节)、实际做法、原因、备选方案、影响。各线只写自己那一节,避免多条线同时改同一段。
## 总控 L
### T0.1 2026-09-30
1. **前端嵌入方式**
- 原条款:DEVELOPMENT 第 3 节 `web/embed.go` 用 `//go:embed all:dist`;TASKS T0.1;`.gitignore` 忽略 `web/dist`。
- 实际做法:默认构建用 `-tags` 以外的 `embed_stub.go` 嵌入 `web/stub/`;`task build` 加 `-tags embeddist` 嵌入真实 `web/dist`。
- 原因:`web/dist` 不进仓库,且要求没有前端产物时也能 `go build`。
- 备选方案:提交最小 `dist`;或构建前脚本生成占位目录。
- 影响:裸 `go build` 不含真实前端;正式产物必须走 `task build`。
2. **listen.addr 写入时机**
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写 `<data_dir>/listen.addr`。
- 实际做法:只要 `serve` 启动成功就写入实际监听地址。
- 原因:测试启动器统一读取该文件更简单,固定端口场景也无害。
- 备选方案:仅当配置端口为 0 时写入。
- 影响:多一个小文件;行为超集,兼容文档要求。
3. **迁移占位**
- 原条款:T0.3 才写完整表与 `VACUUM INTO` 备份;T0.1 允许空执行器加空 0001。
- 实际做法:`internal/store.Migrate` 建 `schema_migrations` 并应用 `0001_init.sql`(内容为 `SELECT 1;`);不做迁移前备份。
- 原因:保证 serve 可跑通迁移路径,表结构留给 T0.3。
- 备选方案:完全空文件 + 只记版本。
- 影响:T0.3 需替换 0001 正文并补备份逻辑;已应用的占位版本号仍为 1。
4. **Taskfile 引入 taskfiles**
- 原条款:TASKS T0.1 / 4.2 引入 `taskfiles/*.yml`。
- 实际做法:`includes: '*': taskfile: taskfiles/*.yml, optional: true`,并放 `_init.yml` 占位。
- 原因:空目录 glob 可能失败;各线稍后加自己的 yml。
- 备选方案:主文件逐条 optional include 各线文件名。
- 影响:无。
5. **deploy/Dockerfile**
- 原条款:完整多架构镜像属 Q4;T0.1 需要 `task docker` 目标。
- 实际做法:提供单架构多阶段 Dockerfile 骨架,供 `task docker` 使用;T0.1 验证另用官方 `golang` 镜像跑 `task check`。
- 原因:让 docker 目标可执行,又不抢 Q4 范围。
- 备选方案:docker 目标仅 echo 提示。
- 影响:镜像发布流程仍由 Q4 定稿。
### T0.2 2026-09-30
1. **请求指纹规范化格式**
- 原条款:DEVELOPMENT 7.3「下列字段规范化后的 SHA-256」,未规定字节布局。
- 实际做法:对 `to.kind`、`to.id`、`body.enc`、有效 `content_type`、解码后正文、`meta` 规范 JSON、`send_at_ms`、`delay_ms`、有效 `offline.keep`/`ttl_seconds`、有效 `receipt` 做长度前缀(或有无标记)串联后算 SHA-256,输出小写十六进制;`meta` 用 `encoding/json` 对 `map` 键排序序列化;不含 `talk_password`、`rid`。
- 原因:文档未给规范格式,需固定、与键顺序无关、含可选字段区分。
- 备选方案:整段规范 JSON 对象再哈希。
- 影响:各语言 SDK / 服务端必须共用同一布局,否则防重失效。
2. **指纹使用文档默认值后的有效字段**
- 原条款:指纹字段列表含 `receipt`、`offline.*`,未说明缺省如何表示。
- 实际做法:`receipt` 缺省按 true;`offline.keep` 缺省 false;`keep` 为 true 且未给 `ttl_seconds` 时按 86400;`keep` 为 false 时 ttl 记 0;`content_type` 按 enc 补默认。
- 原因:重试时省略与显式默认应视为同一请求。
- 备选方案:按原始 JSON 有无字段区分,省略与显式默认算冲突。
- 影响:SDK 省略默认字段时防重仍命中。
3. **JSON Encoder 去掉尾部换行**
- 原条款:用 `json.Encoder` 且 `SetEscapeHTML(false)`。
- 实际做法:Encode 后去掉 `Encoder.Encode` 追加的 `\n`,整帧字节数不含该换行。
- 原因:MQTT 一发布一帧,示例 JSON 无尾换行;保留换行会抬高帧长并与本地 `frame_too_large` 判断不一致。
- 备选方案:保留换行并在 DEVELOPMENT 写明。
- 影响:线上帧比「裸 Encoder.Encode」少 1 字节。
4. **协议包校验范围**
- 原条款:T0.2 要求编号规则、正文/meta 大小、`send_at_ms`/`delay_ms` 互斥、登录密码不以 `nst_` 开头。
- 实际做法:上述必做之外,顺带校验各帧 `v`/`type`/`rid`、目标 kind、分页 limit、致命 reason 枚举等结构字段;业务错误(目标不存在、配额等)不在本包判定。
- 原因:无结构校验则编解码测试无法覆盖「合法帧」边界。
- 备选方案:协议包只做编解码,校验留给各 app 模块。
- 影响:服务端应复用本包 `Validate`,避免重复规则。
### T0.3 2026-09-30
1. **完整表放在 0002,不改已发布的 0001**
- 原条款:TASKS T0.3 / 4.2「`0001_init.sql` 包含第 7.7 节全部表」;T0.1 偏差曾写「T0.3 需替换 0001 正文」。
- 实际做法:保留 `0001_init.sql` 为 `SELECT 1;`;新增 `0002_schema.sql` 写入 DEVELOPMENT 7.7 全部业务表与索引(含 `api_tokens`、`settings`、`session_hash` 等)。`schema_migrations` 仍由迁移执行器 `CREATE TABLE IF NOT EXISTS` 维护,不放入 0002。
- 原因:T0.1 的 0001 可能已记入已有库的 `schema_migrations`;改写已发布迁移语义会导致「版本已应用但表不存在」。
- 备选方案:对未迁移库特殊检测并改写 0001(复杂且易错)。
- 影响:新库会有版本 1+2 两行;与 TASKS「表在 0001」字面不一致,与「不改已发布迁移」一致。
2. **写入队列先做一操作一事务**
- 原条款:DEVELOPMENT 7.2 合并提交(最多 256 或凑满 2ms,SAVEPOINT);TASKS T0.3 允许简单实现,P2 换合并。
- 实际做法:`store.Queue` 用互斥锁串行,每请求一个事务;注释与本条标明 P2 再改为写 goroutine 合并提交。
- 原因:本任务范围;合并留给平台 P2。
- 备选方案:T0.3 直接做合并(抢 P2 范围)。
- 影响:高并发写入落盘次数偏多,正式压测前需完成 P2。
3. **空库不备份;仅已有 db 文件且有未应用版本时 VACUUM INTO**
- 原条款:DEVELOPMENT 7.7「有未应用版本时先 VACUUM INTO」;未区分空库。
- 实际做法:`Open` 在打开前检查 `nixmsg.db` 是否已存在;不存在则跳过备份;存在且有 pending 则写入 `<data_dir>/backup/pre-migrate-<UTC时间>.db`。迁移失败返回错误,不自动从备份恢复。
- 原因:空库备份无意义;失败退出与文档一致,恢复交给运维。
- 备选方案:失败时自动还原备份再退出。
- 影响:与任务说明一致;运维需知备份路径。
### T0.5 2026-09-30
1. **admin init / 管理员密码**
- 原条款:TASKS T0.5「自动执行 `admin init` 拿到管理员密码」;命令行启动器 JSON 含管理员密码。
- 实际做法:定义 `AdminInitializer`(默认 `CLIAdminInit`);二进制尚无 `admin` 子命令时返回 `ErrAdminInitUnsupported`,启动器跳过并继续起 serve;`admin_password` 字段为空字符串。示例集成测试只验 `/healthz`。
- 原因:`admin init` 属 P1,当前 main 仅有 `version`/`serve`。
- 备选方案:harness 内嵌假密码写入库(无表结构可写);或阻塞等 P1。
- 影响:P1 合入后无需改调用方接口,密码解析约定见 `parseAdminPassword`;Q/SDK 集成测试在拿到非空密码前勿依赖管理登录。
2. **MQTT 测试客户端范围**
- 原条款:能收发 DEVELOPMENT 第 6 节应用帧。
- 实际做法:提供 TCP / WebSocket(`/mqtt`,子协议 `mqtt`)传输层 `MQTTClient`,收发原始 MQTT 控制包字节;不实现 CONNECT/hello/主题业务。
- 原因:内置 broker 与协议处理尚未合入(连接 N / 后续任务);T0.5 先给可连传输与占位 API。
- 备选方案:引入完整 MQTT 客户端库并编假 broker。
- 影响:业务级帧测试在 broker 可用后由各线基于 `Send`/`Recv` 或再包一层完成。
3. **进程停止方式**
- 原条款:优雅停机(DEVELOPMENT 7.8 / P1)。
- 实际做法:测试启动器对子进程使用 `Kill`(Windows 上 `Interrupt` 不可靠)。
- 原因:保证并行测试与清理在 Windows 上稳定。
- 备选方案:Unix 发 SIGTERM;Windows 用 Job Object / Ctrl+Break。
- 影响:不覆盖优雅停机验收;该验收仍归 P1/Q。
### T0.4 2026-09-30
1. **broker↔app 契约包放在 `internal/app/port`**
- 原条款:TASKS T0.4「broker 和 app 之间的接口」;示例路径 `internal/app/port` 或 `internal/broker/port`。
- 实际做法:放在 `internal/app/port`:`UplinkHandler`(broker→app)、`Downlink` / `ConnControl`(app→broker)。不依赖 mochi。
- 原因:契约由 app 消费形态主导,避免 broker 包在 N 线实现前成为空壳;N 线实现 broker 时 import 本包即可。
- 备选方案:放在 `internal/broker/port` 或单独 `internal/port`。
- 影响:N/M/I 依赖路径固定为 `internal/app/port`。
2. **接口方法先返回未实现或空操作,不做业务状态机**
- 原条款:T0.4 要求 Go 接口与测试假实现;不要实现真正业务逻辑。
- 实际做法:`auth` / `message` / `identity` / `group` 的写路径假实现返回 `ErrNotImplemented`;调度/推送/清理/在线查询等返回空成功或固定假数据;`wire()` 组装这些假实现,`serve` 仅调用 `RecoverOnStart`(空操作)并保留依赖引用。
- 原因:让后续各线有可编译的替换点,且不抢 P/N/M/I/A 实现范围。
- 备选方案:接口方法全部 panic;或完全不接线 serve。
- 影响:在假实现替换前,端协议与管理 API 仍不可用(本任务预期)。
3. **管理契约补充 `GET /api/admin/groups/{id}`**
- 原条款:DEVELOPMENT 第 8 节路由表列出 groups 的 GET/POST 列表创建与 PATCH/DELETE,未单列群详情。
- 实际做法:`docs/api/admin-api.md` 增加 `GET /api/admin/groups/{id}`(成员分页),供后台详情页使用。
- 原因:改名/解散/成员管理需要详情;与端协议 `group.get` 对称。
- 备选方案:详情拼进列表项或仅用 PATCH 回显。
- 影响:A/W 按契约实现该只读路由。
4. **CSV 导入校验失败时用信封外的 `data.errors`**
- 原条款:写明返回出错行号和原因;未规定 JSON 形状。
- 实际做法:HTTP 400,`ok=false`,`error.code=bad_request`,同行号列表放在顶层 `data.errors`。
- 原因:通用 `error` 只有 code/message,放不下多行明细。
- 备选方案:把明细塞进 `error.message` 字符串。
- 影响:W 线按 `data.errors` 渲染。
## 平台 P
暂无。
## 连接 N
暂无。
## 消息 M
暂无。
## 身份 I
暂无。
## 后台接口 A
暂无。
## 后台网页 W
暂无。
## SDK 一 S1
暂无。
## SDK 二 S2
暂无。
## 测试交付 Q
### Q1 / Q4 骨架 2026-09-30
1. **压测客户端未做到 1000 连接 / 10 分钟**
- 原条款:TASKS Q1「1000 连接的压测客户端」;验证「能保持 1000 个连接」。
- 实际做法:`test/load` 提供 MQTT 3.1.1 CONNECT/CONNACK 骨架与 `mqttbench` 命令,单元测试用进程内假 broker 验证多连接与连接数打印;未对真实 broker 压到 1000,也未跑 10 分钟吞吐。
- 原因:本波只做 Q1 基础设施骨架;完整弱网/压测属 Q3,且当前 main 尚无完整 MQTT broker 业务。
- 备选方案:立刻接 mochi 假 broker 或外部 mosquitto 压到 1000。
- 影响:Q3 需补真实压测与报告数字。
2. **未做弱网压测与验收用例**
- 原条款:TASKS Q2/Q3;本波总控指示「不要做弱网压测和验收用例」。
- 实际做法:只交付 toxiproxy/netem 辅助、报告生成器与空结果样例;F01–F23 全部为未测。
- 原因:波次范围。
- 备选方案:无。
- 影响:交付标准第 2、4 条待后续波次。
3. **为 Docker 健康检查在 `cmd/nixmsg` 增加 `healthcheck`**
- 原条款:DEVELOPMENT 11.2/11.4;TASKS 第 5 节 `cmd/nixmsg` 属平台 P。
- 实际做法:Q 线最小实现 `nixmsg healthcheck`(读 `listen.addr` 或配置 `listen`,请求本机 `/healthz`),否则 compose 健康检查无法按文档工作。
- 原因:Q4 镜像骨架硬依赖该子命令;改动面小。
- 备选方案:等 P 实现后再写 compose;或健康检查改用外部 curl(与 distroless 无 shell/curl 冲突)。
- 影响:P 线后续可替换或扩展实现;注意勿重复注册同名命令。
4. **Docker 镜像仅当前架构骨架,不推送**
- 原条款:DEVELOPMENT 11.4 多架构 `buildx` 推送;TASKS Q4 验证两个架构。
- 实际做法:完善多阶段 Dockerfile + `deploy/docker-compose.yml`(镜像名 `git.asio.asia/nixevol/nixmsg`,健康检查 `/nixmsg healthcheck`);本机只 `docker build` 当前架构并冒烟 `/healthz`;不做 `docker push`、不做 arm64。
- 原因:总控本波只要骨架;推送在 Z3。
- 备选方案:本波强制 buildx 双架构(耗时长、非阻塞目标)。
- 影响:多架构与仓库推送留到 Q4 定稿 / 阶段 3。
5. **compose 示例端口仍写 7443**
- 原条款:测试隔离「不要写死 7443」;DEVELOPMENT 11.4 示例为 `7443:7443`。
- 实际做法:`deploy/docker-compose.yml` 与文档示例一致使用 7443;混沌辅助与压测工具通过参数传入上游地址,不写死;本地冒烟可用其它宿主机端口映射。
- 原因:部署示例需与 DEVELOPMENT 对齐;隔离约束针对并行测试而非产品默认端口。
- 备选方案:compose 用变量 `${NIXMSG_HOST_PORT:-7443}`。
- 影响:多 Agent 同时起官方 compose 会端口冲突,应改映射或错开项目名。
6. **toxiproxy 镜像与 netem 旁路镜像选型**
- 原条款:用 toxiproxy 官方镜像;Linux 丢包用 netem。
- 实际做法:`ghcr.io/shopify/toxiproxy:2.12.0`;netem 说明用 `nicolaka/netshoot` 挂脚本(需 `NET_ADMIN`)。
- 原因:官方镜像无 tc;本机 Windows 不能本机 netem。
- 备选方案:自建含 iproute2 的旁路镜像。
- 影响:首次拉取 netshoot 需网络;脚本不进业务镜像。
7. **增加根目录 `.dockerignore`**
- 原条款:未要求;Dockerfile 原为 `COPY web/` 在 `pnpm install` 之后。
- 实际做法:忽略 `**/node_modules`、`web/dist`、`bin` 等,避免本机 Windows 的 `node_modules` 覆盖 Linux 安装结果导致 `vue-tsc` 找不到。
- 原因:本机 `task check` 会生成 `web/node_modules`,直接进构建上下文会破坏前端阶段。
- 备选方案:Dockerfile 在 `COPY web/` 后再跑一次 `pnpm install`。
- 影响:镜像构建依赖 dockerignore;与任务目录无关的本地产物不再进上下文。
8. **命名卷首次需 chown 给 nonroot**
- 原条款:DEVELOPMENT 11.4「挂载的数据目录要可写」,uid 65532。
- 实际做法:冒烟前对命名卷执行 `chown -R 65532:65532 /data`;compose 未内置 init 容器。
- 原因:空命名卷属主为 root 时,distroless nonroot 无法建库(`unable to open database file`)。
- 备选方案:compose 增加一次性 init 服务;或文档要求宿主机目录预授权。
- 影响:按示例首次 `up` 前需处理权限,否则 serve 立即退出。