16 KiB
16 KiB
实现与文档的偏差
开发中凡是实现和 PRD.md、DEVELOPMENT.md 不一致的地方,以及文档没写清、开发中自己拿主意的地方,都记在这里,交给负责人事后审阅。开发全程自动推进,记下来就继续,不等审阅。
每条写清:日期、原条款(文档和小节)、实际做法、原因、备选方案、影响。各线只写自己那一节,避免多条线同时改同一段。
总控 L
T0.1 2026-09-30
-
前端嵌入方式
- 原条款: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。
- 原条款:DEVELOPMENT 第 3 节
-
listen.addr 写入时机
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写
<data_dir>/listen.addr。 - 实际做法:只要
serve启动成功就写入实际监听地址。 - 原因:测试启动器统一读取该文件更简单,固定端口场景也无害。
- 备选方案:仅当配置端口为 0 时写入。
- 影响:多一个小文件;行为超集,兼容文档要求。
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写
-
迁移占位
- 原条款:T0.3 才写完整表与
VACUUM INTO备份;T0.1 允许空执行器加空 0001。 - 实际做法:
internal/store.Migrate建schema_migrations并应用0001_init.sql(内容为SELECT 1;);不做迁移前备份。 - 原因:保证 serve 可跑通迁移路径,表结构留给 T0.3。
- 备选方案:完全空文件 + 只记版本。
- 影响:T0.3 需替换 0001 正文并补备份逻辑;已应用的占位版本号仍为 1。
- 原条款:T0.3 才写完整表与
-
Taskfile 引入 taskfiles
- 原条款:TASKS T0.1 / 4.2 引入
taskfiles/*.yml。 - 实际做法:
includes: '*': taskfile: taskfiles/*.yml, optional: true,并放_init.yml占位。 - 原因:空目录 glob 可能失败;各线稍后加自己的 yml。
- 备选方案:主文件逐条 optional include 各线文件名。
- 影响:无。
- 原条款:TASKS T0.1 / 4.2 引入
-
deploy/Dockerfile
- 原条款:完整多架构镜像属 Q4;T0.1 需要
task docker目标。 - 实际做法:提供单架构多阶段 Dockerfile 骨架,供
task docker使用;T0.1 验证另用官方golang镜像跑task check。 - 原因:让 docker 目标可执行,又不抢 Q4 范围。
- 备选方案:docker 目标仅 echo 提示。
- 影响:镜像发布流程仍由 Q4 定稿。
- 原条款:完整多架构镜像属 Q4;T0.1 需要
T0.2 2026-09-30
-
请求指纹规范化格式
- 原条款: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 / 服务端必须共用同一布局,否则防重失效。
-
指纹使用文档默认值后的有效字段
- 原条款:指纹字段列表含
receipt、offline.*,未说明缺省如何表示。 - 实际做法:
receipt缺省按 true;offline.keep缺省 false;keep为 true 且未给ttl_seconds时按 86400;keep为 false 时 ttl 记 0;content_type按 enc 补默认。 - 原因:重试时省略与显式默认应视为同一请求。
- 备选方案:按原始 JSON 有无字段区分,省略与显式默认算冲突。
- 影响:SDK 省略默认字段时防重仍命中。
- 原条款:指纹字段列表含
-
JSON Encoder 去掉尾部换行
- 原条款:用
json.Encoder且SetEscapeHTML(false)。 - 实际做法:Encode 后去掉
Encoder.Encode追加的\n,整帧字节数不含该换行。 - 原因:MQTT 一发布一帧,示例 JSON 无尾换行;保留换行会抬高帧长并与本地
frame_too_large判断不一致。 - 备选方案:保留换行并在 DEVELOPMENT 写明。
- 影响:线上帧比「裸 Encoder.Encode」少 1 字节。
- 原条款:用
-
协议包校验范围
- 原条款:T0.2 要求编号规则、正文/meta 大小、
send_at_ms/delay_ms互斥、登录密码不以nst_开头。 - 实际做法:上述必做之外,顺带校验各帧
v/type/rid、目标 kind、分页 limit、致命 reason 枚举等结构字段;业务错误(目标不存在、配额等)不在本包判定。 - 原因:无结构校验则编解码测试无法覆盖「合法帧」边界。
- 备选方案:协议包只做编解码,校验留给各 app 模块。
- 影响:服务端应复用本包
Validate,避免重复规则。
- 原条款:T0.2 要求编号规则、正文/meta 大小、
T0.3 2026-09-30
-
完整表放在 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」字面不一致,与「不改已发布迁移」一致。
- 原条款:TASKS T0.3 / 4.2「
-
写入队列先做一操作一事务
- 原条款:DEVELOPMENT 7.2 合并提交(最多 256 或凑满 2ms,SAVEPOINT);TASKS T0.3 允许简单实现,P2 换合并。
- 实际做法:
store.Queue用互斥锁串行,每请求一个事务;注释与本条标明 P2 再改为写 goroutine 合并提交。 - 原因:本任务范围;合并留给平台 P2。
- 备选方案:T0.3 直接做合并(抢 P2 范围)。
- 影响:高并发写入落盘次数偏多,正式压测前需完成 P2。
-
空库不备份;仅已有 db 文件且有未应用版本时 VACUUM INTO
- 原条款:DEVELOPMENT 7.7「有未应用版本时先 VACUUM INTO」;未区分空库。
- 实际做法:
Open在打开前检查nixmsg.db是否已存在;不存在则跳过备份;存在且有 pending 则写入<data_dir>/backup/pre-migrate-<UTC时间>.db。迁移失败返回错误,不自动从备份恢复。 - 原因:空库备份无意义;失败退出与文档一致,恢复交给运维。
- 备选方案:失败时自动还原备份再退出。
- 影响:与任务说明一致;运维需知备份路径。
T0.5 2026-09-30
-
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 集成测试在拿到非空密码前勿依赖管理登录。
- 原条款:TASKS T0.5「自动执行
-
MQTT 测试客户端范围
- 原条款:能收发 DEVELOPMENT 第 6 节应用帧。
- 实际做法:提供 TCP / WebSocket(
/mqtt,子协议mqtt)传输层MQTTClient,收发原始 MQTT 控制包字节;不实现 CONNECT/hello/主题业务。 - 原因:内置 broker 与协议处理尚未合入(连接 N / 后续任务);T0.5 先给可连传输与占位 API。
- 备选方案:引入完整 MQTT 客户端库并编假 broker。
- 影响:业务级帧测试在 broker 可用后由各线基于
Send/Recv或再包一层完成。
-
进程停止方式
- 原条款:优雅停机(DEVELOPMENT 7.8 / P1)。
- 实际做法:测试启动器对子进程使用
Kill(Windows 上Interrupt不可靠)。 - 原因:保证并行测试与清理在 Windows 上稳定。
- 备选方案:Unix 发 SIGTERM;Windows 用 Job Object / Ctrl+Break。
- 影响:不覆盖优雅停机验收;该验收仍归 P1/Q。
T0.4 2026-09-30
-
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。
- 原条款:TASKS T0.4「broker 和 app 之间的接口」;示例路径
-
接口方法先返回未实现或空操作,不做业务状态机
- 原条款:T0.4 要求 Go 接口与测试假实现;不要实现真正业务逻辑。
- 实际做法:
auth/message/identity/group的写路径假实现返回ErrNotImplemented;调度/推送/清理/在线查询等返回空成功或固定假数据;wire()组装这些假实现,serve仅调用RecoverOnStart(空操作)并保留依赖引用。 - 原因:让后续各线有可编译的替换点,且不抢 P/N/M/I/A 实现范围。
- 备选方案:接口方法全部 panic;或完全不接线 serve。
- 影响:在假实现替换前,端协议与管理 API 仍不可用(本任务预期)。
-
管理契约补充
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 按契约实现该只读路由。
-
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
-
压测客户端未做到 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 需补真实压测与报告数字。
-
未做弱网压测与验收用例
- 原条款:TASKS Q2/Q3;本波总控指示「不要做弱网压测和验收用例」。
- 实际做法:只交付 toxiproxy/netem 辅助、报告生成器与空结果样例;F01–F23 全部为未测。
- 原因:波次范围。
- 备选方案:无。
- 影响:交付标准第 2、4 条待后续波次。
-
为 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 线后续可替换或扩展实现;注意勿重复注册同名命令。
- 原条款:DEVELOPMENT 11.2/11.4;TASKS 第 5 节
-
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。
- 原条款:DEVELOPMENT 11.4 多架构
-
compose 示例端口仍写 7443
- 原条款:测试隔离「不要写死 7443」;DEVELOPMENT 11.4 示例为
7443:7443。 - 实际做法:
deploy/docker-compose.yml与文档示例一致使用 7443;混沌辅助与压测工具通过参数传入上游地址,不写死;本地冒烟可用其它宿主机端口映射。 - 原因:部署示例需与 DEVELOPMENT 对齐;隔离约束针对并行测试而非产品默认端口。
- 备选方案:compose 用变量
${NIXMSG_HOST_PORT:-7443}。 - 影响:多 Agent 同时起官方 compose 会端口冲突,应改映射或错开项目名。
- 原条款:测试隔离「不要写死 7443」;DEVELOPMENT 11.4 示例为
-
toxiproxy 镜像与 netem 旁路镜像选型
- 原条款:用 toxiproxy 官方镜像;Linux 丢包用 netem。
- 实际做法:
ghcr.io/shopify/toxiproxy:2.12.0;netem 说明用nicolaka/netshoot挂脚本(需NET_ADMIN)。 - 原因:官方镜像无 tc;本机 Windows 不能本机 netem。
- 备选方案:自建含 iproute2 的旁路镜像。
- 影响:首次拉取 netshoot 需网络;脚本不进业务镜像。
-
增加根目录
.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;与任务目录无关的本地产物不再进上下文。
- 原条款:未要求;Dockerfile 原为
-
命名卷首次需 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 立即退出。