Files
NixMsg/docs/DEVIATIONS.md
T

16 KiB
Raw Blame History

实现与文档的偏差

开发中凡是实现和 PRD.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 立即退出。