490 lines
34 KiB
Markdown
490 lines
34 KiB
Markdown
# 实现与文档的偏差
|
||
|
||
开发中凡是实现和 [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
|
||
|
||
### P1 2026-09-30
|
||
|
||
1. **admin set-password 传参方式**
|
||
- 原条款:DEVELOPMENT 11.2 仅列命令名,未规定密码如何传入。
|
||
- 实际做法:支持 `--password <pwd>`、位置参数,或从 stdin 读一行;最短 12 位。
|
||
- 原因:自动化测试与 Docker 非交互环境需要非交互传参。
|
||
- 备选方案:仅交互式 prompt。
|
||
- 影响:运维文档需写明推荐用 `--password` 或管道,勿把密码写进 shell 历史时可改用 stdin。
|
||
|
||
2. **admin init 密码字符集**
|
||
- 原条款:生成 20 位密码,未规定字符集。
|
||
- 实际做法:从去掉易混字符(0/O/1/I/l)的字母数字中均匀抽样 20 位。
|
||
- 原因:终端抄写友好。
|
||
- 备选方案:全 ASCII 可打印字符。
|
||
- 影响:熵略低于全字符集,对 20 位仍足够。
|
||
|
||
3. **PHC 哈希辅助先放在 auth,池在 P3**
|
||
- 原条款:argon2 并发池属 P3;admin init 需落库哈希属 P1。
|
||
- 实际做法:P1 在 `internal/auth` 提供 `HashPassword`/`VerifyPassword`(PHC),admin 命令直接调用;P3 再用同参数实现带并发上限的 `HashPool`。
|
||
- 原因:避免 P1 在 cmd 内复制算法,又不等到 P3 才做 init。
|
||
- 备选方案:P1 cmd 内联 argon2;或 P1/P3 合并提交。
|
||
- 影响:P3 合入后 admin 可改为走 HashPool(非必须,单次 init 无并发压力)。
|
||
|
||
4. **优雅停机顺序**
|
||
- 原条款:停接受 → 写队列最多 10 秒 → 断开连接退出。
|
||
- 实际做法:`http.Server.Shutdown`(先停接受并等待进行中的 HTTP)后,再 `Queue.Drain` 最多 10 秒;本期尚无长连接表,断开连接由 Shutdown 覆盖。
|
||
- 原因:当前 main 仅有 HTTP 健康检查;N 线接入后应在 Drain 前后显式踢连接。
|
||
- 备选方案:先关 listener、Drain、再 Shutdown。
|
||
- 影响:有 MQTT 长连接后需 N/P 联调停机路径。
|
||
|
||
### P2 2026-09-30
|
||
|
||
1. **合并写入队列替换 T0.3 简单实现**
|
||
- 原条款:DEVELOPMENT 7.2;TASKS P2。
|
||
- 实际做法:单写 goroutine;批次上限 256 或等待 2ms;每操作用 `SAVEPOINT`/`ROLLBACK TO`/`RELEASE`;Begin/Commit/SAVEPOINT 基础设施失败返回 `errors.Join(ErrBusy, err)` 并令 `IsReady()=false`;业务操作错误只回滚该 SAVEPOINT,不标 busy。
|
||
- 原因:满足每秒约 200 条写入的落盘合并需求。
|
||
- 备选方案:按固定时间窗无条件合并。
|
||
- 影响:调用方需用 `errors.Is(err, store.ErrBusy)` 映射协议 `busy`;`/readyz` 读 `DB.Ready`。
|
||
|
||
2. **调用方 context 取消与已入队任务**
|
||
- 原条款:未规定入队后取消。
|
||
- 实际做法:入队前检查 ctx;批次执行前再检查;若调用方在等待结果时取消,最多再等 30 秒取结果以免泄漏。
|
||
- 原因:写 goroutine 仍可能已执行该操作,不能静默丢结果。
|
||
- 备选方案:取消即从队列摘除(需可取消数据结构)。
|
||
- 影响:极端取消场景下调用方可能多等一会儿。
|
||
|
||
### P3 2026-09-30
|
||
|
||
1. **管理员/注册锁定阈值沿用登录 IP 档**
|
||
- 原条款:登录/对话密码阈值写清;管理员登录与注册安全码仅写「临时锁定」,未给数字。
|
||
- 实际做法:`LockAdminIP`、`LockRegisterIP`、`LockTalkPair`/`LockLoginEndpointIP` 均为 5 分钟窗口 10 次、锁 5 分钟;`LockTalkTarget`/`LockLoginEndpoint` 为 1 小时 50 次、锁 1 小时。
|
||
- 原因:与 PRD D18/F23「默认 5 分钟 10 次」叙述一致。
|
||
- 备选方案:管理员单独更严阈值。
|
||
- 影响:A/I/N 线直接用 `LoginLocks` 即可。
|
||
|
||
2. **真实 auth 实现未改 wire.go**
|
||
- 原条款:平台不改 `wire.go`;P3 实现接口。
|
||
- 实际做法:提供 `NewPool`/`NewSessionTokens`/`NewAPITokens`/`NewLoginLocks`;`wire()` 仍用 Stub,由总控或各线接线时替换。
|
||
- 原因:分工禁止改 wire.go。
|
||
- 备选方案:在 serve 旁路替换(会绕过 wire)。
|
||
- 影响:合入后需有一次接线才能在进程内用上真实哈希池。
|
||
|
||
3. **令牌随机部分用 RawURLEncoding**
|
||
- 原条款:32 字节随机数的 base64url。
|
||
- 实际做法:`encoding/base64.RawURLEncoding`(无 padding)。
|
||
- 原因:URL/Header 友好,与常见 token 惯例一致。
|
||
- 备选方案:StdEncoding 带 padding。
|
||
- 影响:SDK/文档示例需无 `=` 结尾。
|
||
|
||
### P4 2026-09-30
|
||
|
||
1. **指标包放在 `internal/metrics`**
|
||
- 原条款:TASKS 分工表未列 metrics 目录;P4 要求用 prometheus/client_golang 建注册表。
|
||
- 实际做法:新建 `internal/metrics`,提供 `New`/`Handler`/`Registry` 字段供各线打点;不在 `serve` 挂路由(访问规则属 A 线)。
|
||
- 原因:不宜塞进 auth/store/config。
|
||
- 备选方案:放 `internal/httpx`(A 线目录)。
|
||
- 影响:A/N 接线时 import 本包并挂 `/metrics`。
|
||
|
||
2. **指标命名**
|
||
- 原条款:列了指标含义,未规定 Prometheus 名字。
|
||
- 实际做法:`nixmsg_connections{transport}`、`nixmsg_endpoints`、`nixmsg_deliveries_pending`、`nixmsg_messages_scheduled`、`nixmsg_dispatch_to_push_duration_seconds`、`nixmsg_ack_duration_seconds`、`nixmsg_write_queue_length`、`nixmsg_write_batch_commit_duration_seconds`、`nixmsg_password_hash_queue_length`、`nixmsg_errors_total{code}`。
|
||
- 原因:固定可抓取文本便于联调。
|
||
- 备选方案:更短前缀或 HistogramVec。
|
||
- 影响:仪表盘按上述名字配置。
|
||
|
||
## 连接 N
|
||
|
||
### N1 / N2 2026-09-30
|
||
|
||
1. **未接线 `cmd/nixmsg`**
|
||
- 原条款:serve 最终应挂上端口识别、broker、`/mqtt`。
|
||
- 实际做法:本任务只交付 `internal/listener`、`internal/broker`;按总控要求不改 `cmd/nixmsg`。
|
||
- 原因:避免与平台/总控并行改 wire 冲突;合并时再接线。
|
||
- 备选方案:本分支顺带改 `wire.go`(与指令冲突)。
|
||
- 影响:当前 `serve` 仍是 T0.4 的简单 `/healthz` 监听,不含 MQTT。
|
||
|
||
2. **`listen.addr` / `admin.addr` 仅端口为 0 时写入**
|
||
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写地址文件;T0.1 偏差曾改为 always write。
|
||
- 实际做法:`listener.Server` 仅当配置地址端口为 `0` 时写 `listen.addr` / `admin.addr`。
|
||
- 原因:本任务说明与 DEVELOPMENT 4.1 字面一致;T0.1 的 always write 在 `cmd/nixmsg`,本线未改。
|
||
- 备选方案:接线时统一为 always write 以兼容 harness。
|
||
- 影响:固定端口场景下 harness 若只读地址文件会读不到;接线时建议沿用 T0.1 超集或改 harness。
|
||
|
||
3. **登录校验为可替换接口,默认拒绝**
|
||
- 原条款:第 5 节完整会话令牌/密码/锁定属 N3。
|
||
- 实际做法:`broker.Authenticator` 接口 + 默认 `RejectAuthenticator`;内部错误在 `OnConnect` 返回 error;测试提供 `AllowAuthenticator`。
|
||
- 原因:N3 范围;N2 需可跑通装配与钩子。
|
||
- 备选方案:N2 内做假登录表(超出范围)。
|
||
- 影响:真实端连不上直到 N3;总控接线时注入 Authenticator。
|
||
|
||
4. **大帧并发名额释放策略**
|
||
- 原条款:DEVELOPMENT 7.5 大于 64KiB 全局同时不超过 64;PUBACK / 超时 / 断线释放。
|
||
- 实际做法:发布前申请名额;QoS 0 发布成功立即释放;QoS 1 在 `OnQosComplete` 且 payload>64KiB 时释放,断线 `releaseAllLarge`;未单独做「确认超时」计时释放(确认超时属 M 线推送循环)。
|
||
- 原因:N2 无投递确认计时器;与 M 线推送超时释放衔接。
|
||
- 备选方案:broker 内对大帧自建超时(与 M 重复)。
|
||
- 影响:若客户端永不 PUBACK 且不断线,名额可能占满直到断开;M 线超时踢线或回调 Disconnect 可释放。
|
||
|
||
5. **`OnPublishDropped` 仅打日志**
|
||
- 原条款:清「已推送」标记并 1 秒后重推。
|
||
- 实际做法:钩子记录 debug 日志;清标记/重推留给消息 M。
|
||
- 原因:投递状态在 M/store,N2 无投递表。
|
||
- 备选方案:N2 暴露回调给 M 注册。
|
||
- 影响:接线后 M 需订阅或包装该钩子;当前接口可后续加 `OnPublishDropped` 回调字段。
|
||
|
||
## 消息 M
|
||
|
||
### M1 2026-09-30
|
||
|
||
1. **提交时分发做成最小正确版**
|
||
- 原条款:DEVELOPMENT 7.3 步骤 8 / 7.4:`send_at` 已到则同一写操作内完整分发(停用拒绝、`queue_full`、`expire_at`/宽限、无接收者 `completed`、回执等)。
|
||
- 实际做法:单聊只插一条 `pending`;群按当时 `group_members` 去掉发送者各插 `pending`;消息改为 `dispatched`。不设 `expire_at`,不检查接收端配额/在线/停用,不因无接收者改为 `completed`,不写回执,不唤醒推送循环。
|
||
- 原因:M1 范围是提交;完整分发与推送属 M2。
|
||
- 备选方案:M1 直接实现完整 7.4(抢 M2)。
|
||
- 影响:到点消息已有投递行,但停用成员仍会有 `pending`;无成员群仍为 `dispatched` 且无投递;推送需等 M2。
|
||
|
||
2. **请求频率突发容量写死为 100**
|
||
- 原条款:DEVELOPMENT 6.10 每端每秒 50、突发 100;配置示例仅有 `requests_per_second`。
|
||
- 实际做法:`Limits.RequestBurst` 默认 100;`requests_per_second<=0` 时不限速(便于测试)。速率桶挂在 `message.App` 的 `Submit` 入口;`ack`/`receipt_ack` 尚未实现故未接桶。
|
||
- 原因:配置无独立 burst 字段。
|
||
- 备选方案:配置增加 `request_burst`;由连接线在上行统一限流。
|
||
- 影响:改 `requests_per_second` 不改突发;正式接线后若 N 线也限流可能双重计数。
|
||
|
||
3. **未接线 `cmd/nixmsg`**
|
||
- 原条款:可替换 T0.4 假实现。
|
||
- 实际做法:新增 `message.App` 实现 `Submit`;保留 `Stub`;按任务隔离要求未改 `cmd/nixmsg`/`wire.go`。
|
||
- 原因:本任务禁止改 `cmd/nixmsg`;总控接线或后续任务再换。
|
||
- 备选方案:本任务直接改 `wire.go`。
|
||
- 影响:进程内仍用 Stub,需显式构造 `message.New` 才能用真实提交。
|
||
|
||
4. **防重键在、消息行已删时返回 `not_found`**
|
||
- 原条款:防重命中返回原消息当前状态;未写明消息行已被清理时的提交重试行为(状态查询为 `not_found`)。
|
||
- 实际做法:`send_keys` 指纹相同但 `messages` 无行时返回 `not_found`。
|
||
- 原因:无法构造 `send_at`/`state`。
|
||
- 备选方案:在 `send_keys` 冗余存结果快照。
|
||
- 影响:保留期过后的重试不再幂等成功。
|
||
|
||
## 身份 I
|
||
|
||
### I1 2026-09-30
|
||
|
||
1. **注册做成可挂载 Handler,不改 cmd/listener**
|
||
- 原条款:TASKS I1 / DEVELOPMENT 6.9 在 `listen` 上提供 `POST /api/client/register`;依赖 N1 端口识别。
|
||
- 实际做法:`identity.NewRegisterHandler` / `identity.NewServer().Handler()` 返回 `http.Handler`,由接线方 `mux.Handle("/api/client/register", h)`;本线不改 `cmd/nixmsg`、`internal/listener`(N 线未合入)。
|
||
- 原因:隔离交付,避免抢 N/P 接线。
|
||
- 备选方案:本线直接改 `wire.go` 挂路由。
|
||
- 影响:合入后需总控或 N/A 接线才对外可访问。
|
||
|
||
2. **密码哈希与锁定走 auth 接口,本分支用可替换假实现测**
|
||
- 原条款:依赖 P3 argon2 池与锁定计数器。
|
||
- 实际做法:`RegisterConfig.Hash`/`Locks` 注入 `auth.HashPool`、`auth.LoginLocks`;测试用 `auth.NewStubHashPool` + 仅实现 `LockRegisterIP`(5 分钟 10 次)的测试锁定器,不在本线重写 argon2。
|
||
- 原因:P3 尚未在本分支。
|
||
- 备选方案:等 P3 合入后再写 I1。
|
||
- 影响:生产须注入 P3 实现;StubLoginLocks 永不锁定,不能直接用于开放注册。
|
||
|
||
3. **settings 开关取值**
|
||
- 原条款:`settings.registration_enabled`,未规定字符串字面量。
|
||
- 实际做法:`1`/`true`/`yes`/`on`(大小写不敏感)视为开启,其余(含缺省)关闭;安全码键 `registration_code`。
|
||
- 原因:与 store 测试写入的 `"0"`/`"1"` 对齐并兼容常见布尔字面量。
|
||
- 备选方案:仅认 `"1"`。
|
||
- 影响:A 线写注册设置时宜写 `"1"`/`"0"`。
|
||
|
||
4. **客户端 IP**
|
||
- 原条款:DEVELOPMENT 4.5 受信任代理下用 `X-Forwarded-For`。
|
||
- 实际做法:Handler 默认取 `RemoteAddr` 的 host;可通过 `RegisterConfig.ClientIP` 注入。本线不做 `trusted_proxies` 解析(属 listener/接线)。
|
||
- 原因:不改 listener;代理 IP 应由外层在挂载前算好或注入。
|
||
- 备选方案:在 identity 内复制 4.5 逻辑。
|
||
- 影响:经代理部署时接线方必须注入真实 IP,否则锁定按直连 IP 计。
|
||
|
||
5. **生成登录密码长度**
|
||
- 原条款:F01 留空则生成,8–128 字符,不以 `nst_` 开头;未规定生成长度。
|
||
- 实际做法:生成 20 位字母数字;若偶然以 `nst_` 开头则重抽。
|
||
- 原因:与管理员 init 量级接近,满足规则。
|
||
- 备选方案:16/32 位。
|
||
- 影响:无产品行为差异。
|
||
|
||
## 后台接口 A
|
||
|
||
### A1 2026-09-30
|
||
|
||
1. **A1 仅交付可挂载 Handler,未改 `cmd/nixmsg`**
|
||
- 原条款:管理接口由服务进程提供;TASKS 要求路由可挂载。
|
||
- 实际做法:`admin.New(Deps) *Handler` 实现 `http.Handler`,路径为完整 `/api/admin/...`;总控/后续接线在 mux 上 `Handle("/api/admin/", h)` 即可。本任务按隔离要求不改 `cmd/`、`listener`、`protocol`。
|
||
- 原因:与并行线隔离;A1 验收用 httptest。
|
||
- 备选方案:本分支同时改 `serve.go` 挂载(易与 N/P 冲突)。
|
||
- 影响:合入后需在 `serve`/`wire` 挂载并注入真实 DB/Hash/Locks。
|
||
|
||
2. **P3 未合入时的假哈希与本地锁定/令牌生成**
|
||
- 原条款:密码与令牌哈希用 `internal/auth`;锁定与 argon2 池由 P3 实现。
|
||
- 实际做法:依赖 `auth.HashPool` / `auth.APITokens` / `auth.LoginLocks` 接口;测试注入 `auth.StubHashPool`。提供 `admin.MemoryLoginLocks`(5 分钟 10 次锁 5 分钟)与 `admin.RandomAPITokens`(`nxm_`+32 字节 base64url,SHA-256)供本线与测试使用,不实现 argon2。
|
||
- 原因:第 1 波允许用假实现;P3 合入后替换注入即可。
|
||
- 备选方案:阻塞等待 P3。
|
||
- 影响:生产接线应改用 P3 实现;`MemoryLoginLocks`/`RandomAPITokens` 可保留作测试替身。
|
||
|
||
3. **API 令牌 `id` 为字符串**
|
||
- 原条款:`docs/api/admin-api.md` 示例 `"id": 1`(数字)。
|
||
- 实际做法:遵循库表 `api_tokens.id TEXT`,响应 `id` 为 16 字节随机十六进制字符串。
|
||
- 原因:不改已发布迁移;与 schema 一致。
|
||
- 备选方案:另加 INTEGER 列或把数字存成文本并在 JSON 里发数字。
|
||
- 影响:W 线类型应按 `string` 解析令牌 id。
|
||
|
||
4. **尚未实现的管理路由(鉴权中间件已生效,业务返回 501)**
|
||
- 原条款:DEVELOPMENT 第 8 节完整路由表。
|
||
- 实际做法:已实现 `login`/`logout`/`me`/`password` 与 `/api/admin/tokens` 全套。以下路由经鉴权后返回 `501 not_implemented`(属 A2/A3):
|
||
`GET /overview`;`endpoints` 列表/开通/import/batch/详情/改/删/kick/reset-login-password/talk-password/unlock;`registration` GET/PUT;`groups` 全部(含 `GET /groups/{id}`);`messages` 列表与详情;`GET /settings`。
|
||
- 原因:A1 范围仅鉴权、令牌、操作日志。
|
||
- 备选方案:无。
|
||
- 影响:W/集成测试在 A2/A3 前勿依赖这些业务响应。
|
||
|
||
5. **操作日志用 `slog` 结构化字段**
|
||
- 原条款:写结构化日志(操作者、动作、对象、结果、来源 IP)。
|
||
- 实际做法:`Logger.Info("admin_audit", "actor", ..., "action", ..., "object", ..., "result", ..., "ip", ...)`;改状态请求写日志;不写密码/令牌/正文。
|
||
- 原因:文档未规定日志后端。
|
||
- 备选方案:独立 audit 表。
|
||
- 影响:日志采集需按 msg=`admin_audit` 过滤。
|
||
|
||
## 后台网页 W
|
||
|
||
1. **W1–W3 阶段使用内存假数据,不请求真实 `/api/admin`**
|
||
- 原条款:TASKS W1–W3「先按契约用假数据」;admin-api.md 第 11 节。
|
||
- 实际做法:`web/src/api/admin.ts` 统一导出接口函数,内部调用 `mock.ts`;`http.ts` 已实现带 `X-Nixmsg-Request: 1` 的真实请求封装,供 W4 切换。
|
||
- 原因:A 线管理接口尚未合入,页面与契约可并行开发。
|
||
- 备选:用 MSW 拦截 fetch;当前集中换 `admin.ts` 更简单。
|
||
- 假登录口令:`admin` / `adminpassword`(仅本地 mock,不进后端)。
|
||
|
||
2. **列表分页用页码映射 cursor 偏移**
|
||
- 原条款:admin-api 使用 `cursor`/`limit` 游标分页。
|
||
- 实际做法:假数据把 `page` 编成数字偏移 cursor(`String((page-1)*limit)`),`n-data-table` remote 分页照常。
|
||
- 原因:Naive UI 表格以页码交互;契约游标对前端透明即可。
|
||
- 备选:W4 若后端 cursor 非偏移编码,在 `admin.ts` 内适配,页面仍用页码。
|
||
|
||
3. **帮助图标不用独立图标库**
|
||
- 原条款:DEVELOPMENT 2.4 用 `n-tooltip`;协作规则要求只用 Naive UI。
|
||
- 实际做法:`HelpTip` 用带边框的 `?` 文字触发 tooltip,不引入 `@vicons/*`。
|
||
- 原因:避免第二套依赖;视觉已够用。
|
||
- 备选:若设计要求统一图标,可只加 `@vicons/ionicons5` 作图标资源(仍非第二套组件库)。
|
||
|
||
4. **操作日志页本期未做**
|
||
- 原条款:PRD F17 含管理操作日志;admin-api 写明改状态请求记日志,但未单列日志查询路由。
|
||
- 实际做法:W3 菜单不含操作日志页。
|
||
- 原因:契约无列表接口,无法按契约做假数据页。
|
||
- 备选:A 线补查询接口后 W4/后续补页。
|
||
|
||
5. **端「踢下线」放在行内操作,未单独成页**
|
||
- 原条款:PRD F17 含踢下线。
|
||
- 实际做法:端列表行操作调用 `POST .../kick`。
|
||
- 原因:与重置密码、解锁同级的行内动作更贴桌面工作流。
|
||
- 备选:无。
|
||
|
||
## 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 立即退出。
|