Files
NixMsg/docs/DEVIATIONS.md
T

1028 lines
74 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 定稿。
### L-WIRE 2026-09-30
1. **serve 真实接线范围**
- 原条款:TASKS 总控接线;listener/broker/admin/注册/消息周期循环挂到进程。
- 实际做法:`cmd/nixmsg/serve.go` 注入真实 `auth.NewPool`/`NewSessionTokens`/`NewAPITokens`/`NewLoginLocks`,挂注册与管理路由(踢线调 `Session.Kick`),listener 识别 HTTP/WebSocket/`OnMQTT` 裸 TCP,broker `Login`+`Session`,握手/断线/`OnPublishDropped` 接到 `message.App`,周期 `DispatchDue`/`PushPending`/`CleanupOnce`,下行 `PublishDown`。集成测覆盖:管理登录、写 settings 后注册、密码 MQTT 握手拿 `session_token`。
- 原因:第二波收尾;三件验收必须通。
- 备选方案:分文件多阶段接线。
- 影响:进程可对外登录/注册/握手。
2. **未接线 / 未完成部分(已被 L-UPLINK 部分取代)**
- 原条款:上行应用帧完整分发到 identity/group/presence/message 业务方法。
- 实际做法(L-WIRE 当时):`Session` 处理 hello/logout;其余 `HandleUplink` 仍为 Stub。管理注册设置 HTTP(A3)未实现,测试直接写 `settings` 表。
- 原因:本波强制三件验收;完整协议分发属后续波次。
- 备选方案:本波同时实现 HandleUplink 大 multiplex。
- 影响:见 L-UPLINK。
3. **listen.addr 始终写入**
- 原条款:listener N1 仅端口 0 写文件;T0.1 超集为启动即写。
- 实际做法:listener 按 N1 写端口 0;serve 成功后再强制写一次 `listen.addr`(及分离时的 `admin.addr`)。
- 原因:与 harness / T0.1 一致。
- 备选方案:改 listener 始终写。
- 影响:固定端口也会有地址文件。
### L-UPLINK 2026-09-30
1. **HandleUplink 分发到已有业务服务**
- 原条款:TASKS 总控接线;DEVELOPMENT 第 6 节上行帧由服务端处理后回 `resp`;群事件/在线通知/消息走下行。
- 实际做法:`cmd/nixmsg/uplink.go` 的 `appUplink` 在已握手连接上解码并调用 `message`/`identity`/`presence`/`group` 的既有方法;结果编成 `resp` 经 `PublishDown`(QoS 1)回本连接。`Session` 仍独占 `hello`/`self.logout` 与未握手 `not_ready`。`serve` 把四个 App 与 broker Downlink 注入 uplink。不重写业务状态机。
- 原因:main 上业务实现已齐,缺上行入口。
- 备选方案:各包自建 MQTT 钩子(与 port 契约不符)。
- 影响:端侧 send/ack/recall/status/unlock/self.*/presence.*/directory.list/group.* 可走真实进程。
2. **presence.get 未知编号编码**
- 原条款:DEVELOPMENT 6.5「未知编号为 not_found」;`StatusItem.NotFound` 为内部标记。
- 实际做法:`presence.EncodeGetData` 薄封装,resp data 为 `{"items":[...]}`;未知项 `{"id","not_found":true}`,已知项含 `id`/`online`/`since_ms`。
- 原因:Service 层未规定 JSON 形状,接线需固定可编解码形式。
- 备选方案:整请求失败 `not_found`;或把 `not_found` 塞进 `online` 旁字符串字段。
- 影响:SDK 若只认 items 数组两种形状均可(Go SDK 已兼容 wrap/array)。
3. **resp 超限改发 response_too_large**
- 原条款:DEVELOPMENT 7.5「resp 超限改发 response_too_large」。
- 实际做法:发布前按连接表 `MaxReceiveBytes` 与 `MaxPacketSize` 取较小正上限;超限则改发错误 resp(不再发原 data)。未做 MQTT 包头开销扣减(与 message 推送里的 overhead 预算不完全同一函数)。
- 原因:接线层最小可用检查。
- 备选方案:复用 `message.effectivePayloadLimit`(未导出)。
- 影响:接近包上限的大分页可能比推送路径略严或略松。
4. **断线清 presence.watch**
- 原条款:presence.watch 断线清空。
- 实际做法:`OnDisconnect` 额外调 `ClearWatch`;`SetOffline` 路径本身也会清。顶号旧连接未走 `SetOffline(isCurrent)` 时仍能清订阅。
- 原因:避免旧连接订阅泄漏。
- 备选方案:仅依赖 Session 对 isCurrent 调 SetOffline。
- 影响:无。
5. **仍未接线 / 本波未覆盖**
- 管理注册设置 HTTP(A3)仍未挂;集成测继续写 `settings` 表开注册。
- 下行通知帧(`msg`/`receipt`/`revoked`/`presence`/`group_event`/`fatal`)不是上行分发对象,由既有服务经 PublishDown 发出。
- `self.logout`/`hello` 仍在 Session,不经 appUplink。
- 身份 I5 停用/删除级联、管理端群等非本任务范围。
- 集成测覆盖:双端单聊、离线 keep 后上线、群发不含发送者、延迟内撤回对方无回调;未穷尽 presence.watch 通知与全部 group.* 变体。
### 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` 回调字段。
### N3 2026-09-30
1. **仍未接线 `cmd/nixmsg`**
- 原条款:serve 最终应挂上真实 Authenticator / Session。
- 实际做法:交付 `broker.Login`、`broker.Session` 与 F02 测试;不改 `cmd/nixmsg`/`wire.go`。
- 原因:与总控/其他线并行改 wire 冲突;N1/N2 已约定合并时接线。
- 备选方案:本分支改 wire(与隔离指令冲突)。
- 影响:进程默认仍 RejectAuthenticator,需接线注入 `Login`+`Session`。
2. **`session_hash` 存十六进制文本**
- 原条款:库中存 SHA-256;列为 TEXT,未规定编码。
- 实际做法:存 32 字节哈希的小写 hex(与后台 API 令牌存法一致)。
- 原因:TEXT 列无法直接存原始字节;hex 便于排查。
- 备选方案:BLOB 列或 base64。
- 影响:其他线读写 `session_hash` 需按 hex 编解码。
3. **上下线通知走 `PresenceSink` + port 回调**
- 原条款:写 `online_since`/`offline_since` 并通知;通过现有 port 接口供身份线订阅。
- 实际做法:N3 自己写时间戳;可选注入 `PresenceSink`(对齐 `presence.Service.SetOnline/SetOffline`);并继续调用 `OnHandshakeComplete`/`OnDisconnect`。旧连接断开用连接代号判断,只有当时仍是 current 才标离线。
- 原因:I3 尚未合入,不能依赖具体 presence 实现;双通道便于接线。
- 备选方案:只靠 port、由 I 线写库(与「N3 写 online_since」字面不符)。
- 影响:接线时避免 I 线重复写同一时间戳即可。
4. **InlineClient 的 `OnPublish` 必须放行**
- 原条款:客户端上行 `OnPublish` 返回 `CodeSuccessIgnore`。
- 实际做法:`cl.Net.Inline` 时原样返回,不 Ignore,否则 `PublishDown` 无法送达订阅者。
- 原因:mochi `Publish` 经 InlineClient `InjectPacket` 再进 `OnPublish`。
- 备选方案:不用 InlineClient,改直接 `publishToClient`(偏离文档装配)。
- 影响:N2 既有 PublishDown 测试此前未读回包,此缺陷在 N3 才暴露并修复。
5. **管理员踢线类入口挂在 `Session`**
- 原条款:停用/删除/重置密码先 fatal 再断开;踢下线只断开。
- 实际做法:`Session.Disable`/`Deleted`/`ResetPassword`/`Kick` 可调用;管理 HTTP 未接。
- 原因:A2 管理接口尚未接线。
- 备选方案:放到 `internal/admin`(超出 N 目录)。
- 影响:A/I 接线时调用这些方法即可。
## 消息 M
### M1 2026-09-30
1. **提交时分发做成最小正确版**(已被 M2 取代)
- 原条款:DEVELOPMENT 7.3 步骤 8 / 7.4:`send_at` 已到则同一写操作内完整分发。
- 实际做法(M1):单聊/群插 `pending` 后改 `dispatched`,不做停用/配额/宽限。
- 现状(M2):`Submit` 到点与 `DispatchDue` 均走完整 `dispatchFullTx`(7.4)。
- 原因 / 备选 / 影响:见 M2。
2. **请求频率突发容量写死为 100**
- 原条款:DEVELOPMENT 6.10 每端每秒 50、突发 100;配置示例仅有 `requests_per_second`。
- 实际做法:`Limits.RequestBurst` 默认 100;`requests_per_second<=0` 时不限速(便于测试)。速率桶挂在 `message.App` 的 `Submit` 入口;`ack`/`receipt_ack` 不计入桶(与 6.10 一致)。
- 原因:配置无独立 burst 字段。
- 备选方案:配置增加 `request_burst`;由连接线在上行统一限流。
- 影响:改 `requests_per_second` 不改突发;正式接线后若 N 线也限流可能双重计数。
3. **未接线 `cmd/nixmsg`**
- 原条款:可替换 T0.4 假实现。
- 实际做法:`message.App` 实现 Service;保留 `Stub`;未改 `cmd/nixmsg`/`wire.go`。
- 原因:本任务隔离;总控接线。
- 备选方案:本任务直接改 `wire.go`。
- 影响:进程内仍用 Stub,需显式 `message.New` 并注入 `Downlink`/`ConnRegistry`。
4. **防重键在、消息行已删时返回 `not_found`**
- 原条款:防重命中返回原消息当前状态;未写明消息行已被清理时的提交重试行为。
- 实际做法:`send_keys` 指纹相同但 `messages` 无行时返回 `not_found`。
- 原因:无法构造 `send_at`/`state`。
- 备选方案:在 `send_keys` 冗余存结果快照。
- 影响:保留天数 0 完成后重试不再幂等成功(与 F18 防重「记录还在时」一致)。
### M2 / M3 / M4 2026-09-30
1. **下行与在线用可注入接口,测试用假实现**
- 原条款:推送经 broker `Downlink`;连接表在 N 线内存。
- 实际做法:`WithDownlink` / `WithConnRegistry`;测试用 `RecordingDownlink`、`MemoryConns`。状态机全在 `message` 包。未接真实 MQTT/mochi。
- 原因:N3 握手与 wire 本波未强制合入;任务允许假下行。
- 备选方案:直接依赖 `internal/broker.Broker`。
- 影响:接线方需在握手/断线时调用 `OnHandshakeComplete`/`OnDisconnect`,登记连接,并把 `OnPublishDropped` 转到 `App`。
2. **大帧并发名额在 message 包再管一份**
- 原条款:大于 64KiB 全局同时不超过 64(DEVELOPMENT 7.5);N2 broker 已有信号量。
- 实际做法:`App` 内另有容量 64 的 `largeSem`,发布前申请,确认/超时/清标记时释放。
- 原因:假 `Downlink` 不经 broker 时仍要满足上限。
- 备选方案:只依赖 broker,测试也走真实 PublishDown。
- 影响:接线真实 broker 后可能双重限流(更严,不破坏语义)。
3. **确认超时按库内 `pushed_at` 判定,不另开每连接计时器 goroutine**
- 原条款:推送循环在内存里计时。
- 实际做法:`PushPending` 开头扫描该连接已推且 `now - pushed_at >= ack_timeout` 的投递,再按 keep/expire 规则处理。
- 原因:与崩溃恢复一致、测试可拨钟;避免无调度器时泄漏计时器。
- 备选方案:每连接 `time.AfterFunc`。
- 影响:需周期性调用 `PushPending`(或 `WakePush`)才会触发超时。
4. **后台调度/清理循环未在 App 内自启**
- 原条款:调度按 `send_at` 唤醒;清理约每秒;推送每连接一循环。
- 实际做法:导出 `DispatchDue`、`PushPending`、`CleanupOnce`、`RecoverOnStart`、`WakePush`;由接线方起 goroutine。`WakePush` 在有连接时异步 `PushPending`。
- 原因:未改 `cmd/nixmsg`;避免无 context 的后台泄漏。
- 备选方案:`App.Start(ctx)` 内启三循环。
- 影响:未接线则定时消息不会自动到点,需外部调用 `DispatchDue`。
5. **回执推送窗口未单独记 inflight**
- 原条款:回执窗口默认 64,确认一笔再推下一笔。
- 实际做法:按 `acked=0` 取最多 `ReceiptWindow` 条尽力发布;不因未 `receipt_ack` 停推后续。
- 原因:简化;回执可重复、SDK 按 `receipt_id` 去重。
- 备选方案:内存记已推未确认回执数。
- 影响:发送方慢确认时可能多推几条回执(协议允许重复)。
6. **`Status` 返回自建 map,非独立协议类型**
- 原条款:6.4 状态响应字段。
- 实际做法:`map[string]any`(`state`/`reason`/`counts`/`deliveries`/`next_cursor`)。
- 原因:`protocol` 无 StatusData 结构且不可改共享协议包时取稳妥形状。
- 备选方案:总控在 `protocol` 增类型。
- 影响:接线编码 `resp.data` 时直接 Marshal 该 map 即可。
## 身份 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 位。
- 影响:无产品行为差异。
### I2 / I3 / I4 2026-09-30
1. **服务方法可直接调用,未接 MQTT 分发**
- 原条款:端协议帧经 broker 上行分发到 app。
- 实际做法:`identity.App` / `presence.App` / `group.App` 实现 Service 方法;单测直接调用,不经 mochi。`cmd/nixmsg`/`wire.go` 未改。
- 原因:任务要求可被协议分发调用的服务方法 + 直接调用验证;N3 连接事件与总控接线另波。
- 备选方案:本分支顺带改 wire(与隔离冲突)。
- 影响:合入后需总控/N 接线 HandleUplink → 各 Service;MQTT 帧路径未测。
2. **在线状态:库字段 + 可注入连接表 + 本包握手表**
- 原条款:在线以真实连接为准;依赖 N3 连接事件。
- 实际做法:`presence.ConnTable` 可注入;另用 `SetOnline`/`SetOffline` 维护内存表并写 `endpoints.online_since`/`offline_since`;查询优先 ConnTable,其次内存表,再回退库字段(`online_since` 晚于 `offline_since` 或后者为空)。
- 原因:N3 可能尚未合入,不阻塞 I3。
- 备选方案:阻塞等 N3。
- 影响:未接线时须调用 SetOnline/SetOffline 或写库字段;拔网线心跳超时属 N 线,本波单测不覆盖。
3. **presence / group_event 经 Downlink QoS 0,可 nil**
- 原条款:上下线与群事件尽力推送、不落库。
- 实际做法:注入 `port.Downlink` 时编码帧并 `PublishDown`(presence/group_event QoS 0;退群已推送投递的 `revoked` 用 QoS 1);Downlink 为 nil 时跳过推送,业务库操作仍完成。
- 原因:无 MQTT 时仍可测库逻辑。
- 备选方案:强制假 Downlink。
- 影响:接线后必须注入真实 Downlink 才有通知。
4. **self.logout 踢线可选**
- 原条款:回 resp 后断开连接。
- 实际做法:清 `session_hash`;若注入 `port.ConnControl` 则 `Disconnect`,否则仅清令牌。
- 原因:未接 broker。
- 备选方案:无。
- 影响:接线方应注入 ConnControl。
5. **session_hash 存 SHA-256 十六进制**
- 原条款:库中存会话令牌 SHA-256。
- 实际做法:`hex.EncodeToString(hash)` 写入 TEXT 列。
- 原因:文档未规定编码;十六进制便于调试与比对。
- 备选方案:BLOB/Base64。
- 影响:N3 校验须用同一编码。
6. **self.update 空 name 不写库**
- 原条款:可更新 name。
- 实际做法:`name` 非空才 UPDATE name;仅改 `default_delay_ms` 时不碰 name(JSON omitempty 无法区分省略与空串)。
- 原因:避免误清空名称。
- 备选方案:用指针字段区分。
- 影响:端无法通过协议把名称改成空字符串(可用空格等)。
7. **进群密码校验不产生单聊授权**
- 原条款:拉人须当次带密码;已有授权不能代替。
- 实际做法:`CheckTalkPasswordForJoin` 只校验,不写 `talk_grants`。
- 原因:与 F15「进群仍要密码」一致,避免进群副作用放宽单聊。
- 备选方案:校验成功顺带写 password 授权。
- 影响:仅进群成功后,单聊仍须 unlock/发送带密。
8. **群作废在 group 包内写 deliveries/messages**
- 原条款:退群/踢人/解散的投递作废属 7.6,消息线亦相关。
- 实际做法:I4 在 `group` 写操作里直接改 `pending→rejected`、`scheduled→completed/group_dissolved`,删正文行,需要时插消息级回执,已推送则经 Downlink 发 `revoked`。
- 原因:I4 验收依赖作废规则;M 线完整推送循环可能未合入。
- 备选方案:只调 message 钩子(接口尚未暴露)。
- 影响:与后续 M 作废路径需保持同语义,避免重复作废。
9. **UnlockTalk / SelfChangeLoginPassword / CheckTalkPasswordForJoin 增加 remoteIP**
- 原条款:锁定按发送方+对方 / 编号+IP。
- 实际做法:Service 方法增加 `remoteIP` 参数供锁定计数;T0.4 Stub 同步改签名。
- 原因:无 ConnInfo 的直接调用测试需要显式 IP。
- 备选方案:塞进 context。
- 影响:协议分发接线时从 `ConnInfo.RemoteIP` 传入。
10. **F06 群发断言与 M2 分发语义对齐(2026-09-30)**
- 原条款:F06 验收「发到当时成员」;早期单测在仅写 `pending` 的桩分发下断言 Submit 返回 `dispatched`。
- 实际做法:成员离线且默认不保留时,投递立即 `dropped`,消息为 `completed`(DEVELOPMENT 7.4/7.6);`TestF06GroupSendMembership` 改断言 `completed`;另加 `TestF06GroupSendKeepOfflinePending`:选离线保留时期望 `dispatched` 且接收者有 `pending`。不改消息分发实现。
- 原因:合入含 M2 完整分发的 main 后,旧断言与产品规则冲突;永远 `dispatched` 才是错的。
- 备选方案:测试里注入在线连接表使默认不保留也走 pending(与「离线不保留」场景重复覆盖)。
- 影响:仅测试期望;产品行为不变。
### I5 2026-09-30
1. **停用/删除级联在 identity 包内完成,admin 可选注入**
- 原条款:DEVELOPMENT 7.6 / PRD F01;TASKS I5「提供给 A 线调用」。
- 实际做法:`identity.App.Disable`/`Enable`/`Delete` 在一个写操作里完成启停、清令牌、作废消息/投递、发 `revoked`(可选 Downlink)、退群/转让群主/解散、清授权/回执/防重/发出记录。`admin.Deps.Identity` 非空时,`setEndpointEnabled`/`deleteEndpointBasic`(及 PATCH enabled)委托上述方法;为空时保留 A2 仅改库行为。未改 `cmd/nixmsg`、未改消息上行分发。
- 原因:隔离要求不改接线;A 线已有路由,只接级联。
- 备选方案:在 admin 内复制级联 SQL;或强制 Identity 必填。
- 影响:生产须在挂载 admin 时注入 `identity.App`(及 KickEndpoint/ConnControl/Downlink),否则停用/删除仍无完整作废。
2. **消息级作废回执 state 用 `rejected`**
- 原条款:DEVELOPMENT 6.4 消息级作废写 `endpoint_id` 空、`state=rejected`;I4 解散群对 scheduled 曾写 `completed`。
- 实际做法:I5 对「发给停用/删除端的 scheduled 单聊」及删除时解散群的 scheduled,回执 `state=rejected`,原因分别为 `endpoint_*` / `group_dissolved`。
- 原因:与 6.4 字面一致。
- 备选方案:与 I4 一样写 `completed`。
- 影响:后台/SDK 若按 state 过滤回执需同时认 rejected。
3. **删除时群主转让按 `joined_at` 最早,并列按编号**
- 原条款:转给最早加入的其他成员。
- 实际做法:`ORDER BY joined_at ASC, endpoint_id ASC LIMIT 1`。
- 原因:同时加入时需稳定次序。
- 备选方案:仅按 joined_at。
- 影响:同毫秒加入时编号小者优先。
## 后台接口 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. **A1 范围外路由当时返回 501(A2 已实现端管理)**
- 原条款:DEVELOPMENT 第 8 节完整路由表。
- 实际做法(A1 当时):已实现 `login`/`logout`/`me`/`password` 与 `/api/admin/tokens` 全套;其余经鉴权后 `501`。
- A2 起:端相关路由已改为真实现,见下节;仍为 501 的有 `overview`、`registration`、`groups`、`messages`、`settings`(属 A3)。
- 原因:A1 范围仅鉴权、令牌、操作日志。
- 备选方案:无。
- 影响:W/集成测试在 A3 前勿依赖尚未实现的业务响应。
5. **操作日志用 `slog` 结构化字段**
- 原条款:写结构化日志(操作者、动作、对象、结果、来源 IP)。
- 实际做法:`Logger.Info("admin_audit", "actor", ..., "action", ..., "object", ..., "result", ..., "ip", ...)`;改状态请求写日志;不写密码/令牌/正文。
- 原因:文档未规定日志后端。
- 备选方案:独立 audit 表。
- 影响:日志采集需按 msg=`admin_audit` 过滤。
### A2 2026-09-30
1. **停用/删除完整级联留给 I5**
- 原条款:PRD F01 / DEVELOPMENT 7.6:停用作废未送达消息与发送中消息;删除另含退群、群主转让/解散、清授权与回执/发出记录等。
- 实际做法:A2 直接写库:停用立刻 `enabled=0` 并清空 `session_hash`;删除删 `endpoints` 行并清相关 `talk_grants`;二者均调用可注入的 `Deps.KickEndpoint` 踢连接。不作废投递/消息、不转让群主、不清理群成员与回执。
- 原因:本分支尚无 I5;TASKS 允许先接现有存储并在偏差中写明。
- 备选方案:阻塞等待 I5;或在 A 线内复制级联 SQL(易与 I5 重复冲突)。
- 影响:合入 I5 后应由身份服务 `Disable`/`Delete` 接管级联;总控接线把 kick 钩子接到 broker。`KickEndpoint` 为 nil 时踢线为 no-op(单元测试可注入)。
2. **在线状态读库字段,不依赖 presence 服务**
- 原条款:列表含是否在线、最近上下线;可按在线筛选。
- 实际做法:用 `endpoints.online_since` / `offline_since` 判定在线(`online_since` 非空且大于 `offline_since` 或后者为空);列表项带 `online` / `online_since_ms` / `offline_since_ms`。
- 原因:presence/N3 未在本 Handler 注入;库字段是契约字段。
- 备选方案:注入 `presence.Service.IsOnline`。
- 影响:上下线列未由 N/I 写入前,列表会显示离线。
3. **`login_locked` 仅反映按编号的登录锁定**
- 原条款:列表含 `login_locked`。
- 实际做法:查 `LockLoginEndpoint`;不枚举「编号+IP」锁定。`unlock` 仍调用 `ClearEndpoint` 清两种。
- 原因:`LoginLocks` 接口无「是否任一 IP 锁定」查询。
- 备选方案:扩展 locks 接口。
- 影响:仅 IP 档锁定时列表可能仍显示未锁定,但 unlock 可解除。
4. **A2 时未挂载到 `cmd/nixmsg`;L-WIRE / A3 已接线**
- A2 当时:可挂载 Handler;接线与 `KickEndpoint` 注入留给总控。
- 现状:`serve` 已挂载 Admin Handler;A3 起注入 `Groups`/`Config`/`Version`/`KickEndpoint`。
- 影响:无。
### A3 2026-09-30
1. **概览增加 `endpoints_self`(自助注册数)**
- 原条款:PRD F17「端数量(其中自助注册的数量)」;`admin-api.md` 示例未列该字段。
- 实际做法:`GET /api/admin/overview` 同时返回契约字段与 `endpoints_self`(`source='self'` 计数)。
- 原因:以 PRD / 本任务说明为准补齐。
- 备选方案:只返回 api 文档字段,自助数由前端筛端列表。
- 影响:W 线可选用该字段;旧 mock 类型可增补。
2. **群列表/详情直接读库;变更走 `group.Service`**
- 原条款:群操作调用 `internal/app/group` 已有方法。
- 实际做法:创建/加人用 `AdminCreate`/`AdminAddMembers`;改名/解散/移除/转让先查群主再以群主为 actor 调 `Rename`/`Dissolve`/`Remove`/`Transfer`。列表与详情(含 `joined_at_ms`)因 `List`/`Get` 按成员可见且 Get 无 joined_at,改为管理侧 SQL。
- 原因:端协议 API 按成员视角,后台需全局列表。
- 备选方案:在 group 包增加 AdminList/AdminGet。
- 影响:列表不依赖 Groups 注入;写操作未注入 Groups 时返回 `503 busy`。
3. **后台建群不接受自定义 `id`**
- 原条款:admin-api「id 留空则生成」。
- 实际做法:`AdminCreate` 无自定义 id 参数;请求带非空 id 返回 `400`。
- 原因:不改身份线接口签名。
- 备选方案:扩展 `AdminCreate` 接受可选 id。
- 影响:后台只能服务器生成群编号。
4. **`/metrics` 门禁抽到 `internal/httpx.MetricsGate`**
- 原条款:A 线负责 metrics 访问规则(DEVELOPMENT 4.3)。
- 实际做法:规则实现放 `httpx.MetricsGate`;`listener.NewMux` RoleShared 调用之(N 目录一行替换)。单独后台监听仍直接挂 Handler 不鉴权。
- 原因:A 负责目录是 `admin`+`httpx`;listener 仅接线。
- 备选方案:门禁留在 listener。
- 影响:`serve` 已传 `MetricsToken`;共用端口无令牌 404、错令牌 401。
5. **注册安全码写入审计不含明文**
- 原条款:安全码明文返回已登录管理员,不写日志。
- 实际做法:GET/PUT 响应含 `code`;`admin_audit` 的 object 为空,不记安全码。存库键 `registration_enabled`=`1`/`0`,与 I1 一致。
- 原因:D14 / DEVELOPMENT 12 节。
- 备选:无。
- 影响:无。
6. **消息查询不碰 `message_bodies`**
- 原条款:只读 messages 与 deliveries;响应无正文。
- 实际做法:SELECT 不含 `body`/`body_enc` 以外的正文列(messages 仍有 `body_enc` 列但查询不选它);不 JOIN `message_bodies`。
- 原因:任务硬性要求。
- 备选:无。
- 影响:无。
## 后台网页 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,不进后端)。
- **W4 起已切换**:生产与 e2e 走真实 `/api/admin`;组件 Vitest 通过 `vi.mock("@/api/admin")` → `admin-mock.ts` 仍用假数据。登录页假数据提示已去掉。
2. **列表分页用页码映射 cursor 偏移**
- 原条款:admin-api 使用 `cursor`/`limit` 游标分页。
- 实际做法:假数据把 `page` 编成数字偏移 cursor(`String((page-1)*limit)`),`n-data-table` remote 分页照常。
- 原因:Naive UI 表格以页码交互;契约游标对前端透明即可。
- 备选:W4 若后端 cursor 非偏移编码,在 `admin.ts` 内适配,页面仍用页码。
- **W4**:后端 cursor 为不透明字符串;首页(空 cursor)主路径 e2e 通过。翻到第 2 页若用数字偏移可能无效,未改页面交互;后续若要稳定翻页,应在 `admin.ts` 缓存服务端 `next_cursor`。
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`。
- 原因:与重置密码、解锁同级的行内动作更贴桌面工作流。
- 备选:无。
### W4 2026-09-30
1. **接真实接口并适配 A 线字段**
- 原条款:TASKS W4;admin-api 令牌 `id` 示例为数字。
- 实际做法:`admin.ts` 全部改为 `requestAdmin`;令牌 `id` 类型改为 `string`(对齐 A1 TEXT);`talk-password` 用 `PUT`;概览可选 `endpoints_self`;建群省略空 `id`(A3 不接受自定义 id)。不改服务器。
- 原因:与已合并 A 线实现一致。
- 备选:无。
- 影响:组件测用 `admin-mock.ts`;页面逻辑不变。
2. **未完成投递由 e2e 用 MQTT seed 造出**
- 原条款:DEVELOPMENT 第 13 节后台主路径「看到未完成记录」;管理 messages 只读。
- 实际做法:Playwright 流程里用管理接口开通第二端后,运行 `web/e2e/seedpending`(MQTT hello + 未来 `send_at_ms` 的 send),再打开投递记录页断言 `scheduled`。测完杀进程并删临时目录。
- 原因:管理 API 不能写消息;不改服务器业务代码。
- 备选:插入 SQLite(并发/锁风险);或依赖已有记录(空库无)。
- 影响:e2e 依赖本机 `go` 与 Chromium。
3. **Playwright 挂入 `task itest` / `task w:e2e`**
- 原条款:TASKS W4「端到端测试在 task itest 里通过」;Taskfile 由总控维护、各线写 `taskfiles/<线>.yml`。
- 实际做法:新增 `taskfiles/w.yml`;主 `Taskfile.yml` 显式 `includes.w`(本机 Task 3.53 下 `taskfiles/*.yml` glob 未挂上各线任务)并把 `itest` 在 `go test ./test/...` 后追加 `task: w:e2e`。Playwright 的 `webServer` 先于 `globalSetup` 启动,故用 `e2e/start-stack.mjs` 同时起临时 nixmsg 与 Vite;代理目标 `NIXMSG_PROXY_TARGET`(开发默认仍 7443)。浏览器用 `pnpm exec playwright install chromium`,不改全局配置。
- 原因:满足 W4 验收且隔离安装。
- 备选:仅文档要求手跑 e2e;未采用。
- 影响:`task itest` 变长;需本机 Go/Node/Chromium。
### S1.1 传输层可注入假实现(Go / JS)
- 相关文档:DEVELOPMENT 第 9 节单元测试要求「用假的 MQTT/HTTP,不要起真实服务器」。
- 实际做法:Go 与 JS SDK 均以 `transport` 接口隔离 MQTT;单测注入 `FakeTransport`,自动回复 `hello`、可注入下行帧与 `resp`。真实路径仍分别走 autopaho / MQTT.js。
- 原因:否则无法在无服务端时稳定覆盖 Clean Start、去重再 ack、本地超限、令牌回调、重交不变号。
- 备选:起嵌入式 mochi;被否是因为任务明确禁止真实服务器,且会与并行 Agent 抢端口。
### S1.2 Go autopaho `OnConnectionUp` 内握手改异步
- 相关文档:autopaho 要求 `OnConnectionUp` 不得阻塞;DEVELOPMENT 第 9 节要求订阅 down 后发 `hello` 并等待成功。
- 实际做法:订阅与 `hello` 放在 `OnConnectionUp` 触发的 goroutine 中;`Connect` 轮询 `handshook` 直至超时(默认 30s)。
- 原因:在回调里同步 `hello` 会违反库约束并可能死锁。
- 备选:自定义连接循环不用 autopaho 的 `OnConnectionUp`;未采用,因文档指定 autopaho。
### S1.3 重连退避与「稳定在线 60s」状态机自管
- 相关文档:DEVELOPMENT 第 9 节退避规则。
- 实际做法:自实现 `reconnectBackoff`(1s 起、加倍、上限 30s、±30% 抖动;在线满 60s 将 base 恢复为 1s)。Go 将其接到 autopaho 的 `ReconnectBackoff`;JS 因 `reconnectPeriod: 0` 自管重连循环并使用同一算法。
- 原因:autopaho 自带指数退避参数模型与文档不完全一致,且每次 `establishServerConnection` 会重置 attempt,无法单独表达「未稳定在线则跨周期继续抬升」。
- 备选:直接用 `autopaho.NewExponentialBackoff`;未采用,以免与文档抖动与 60s 恢复语义偏离。
### S1.4 Go 模块许可证标注
- 相关文档:DEVELOPMENT 第 9 节「Go 写 SEE LICENSE,包里带上仓库根目录 LICENSE」。
- 实际做法:`sdk/go/LICENSE` 为仓库根 `LICENSE` 副本;`doc.go` 注明专有许可见 LICENSE(Go modules 无 npm 式 license 字段)。
- 原因:go.mod 无标准 license 键。
- 备选:另加 `LICENSE.md` 指向根目录相对路径;副本更利于 `go get` 后独立阅读。
### S1.5 JS 回调命名与文档概念名
- 相关文档:第 9 节概念方法名 `onSession` / `onMessage` 等。
- 实际做法:TypeScript 对外提供 `onSessionHandler`、`onMessageHandler` 等,避免与 EventEmitter 风格或属性赋值混淆;语义与文档一致。
- 原因:`onSession` 作方法名在部分风格指南中易被误认为事件订阅属性。
- 备选:完全同名方法;可在任务 5 文档化时再加别名。
### S1.6 任务 1–3 当时未做范围(已被 S1.7 取代)
- 当时:任务 4 真实服务器接入清单、任务 5 README/示例/打包试跑未做。
- 现况:见 S1.7。
### S1.7 2026-09-30 任务 4–5 接入清单与打包文档
1. **集成测试自建启动器(不 import 根模块 harness)**
- 原条款:TASKS 用 T0.5 harness 起真实服务端。
- 实际做法:`sdk/go` / `sdk/js` 各自在测试里向上查找含 `cmd/nixmsg` 的仓库根,编译二进制,`admin init` + `listen: 127.0.0.1:0` + 临时 `data_dir` 后 `serve`;注册开关与安全码走管理 `PUT /api/admin/registration`。
- 原因:`sdk/go` 是独立 Go 模块,从该目录跑测时 `harness.Binary` 会先命中 `sdk/go/go.mod` 找不到 `./cmd/nixmsg`;JS 也无法直接 import Go 包。
- 备选:给 harness 加 `NIXMSG_ROOT` 环境变量;未改共享目录以免越界。
- 影响:行为与 harness 一致,仅实现重复约百行。
2. **清单第 4 条「ack 丢失后自动再确认」**
- 原条款:模拟 ack 丢失后服务器重推,SDK 自动再确认且不重复回调。
- 实际做法:真实服务用 `ManualAck` + `keep` 消息:收一次不 ack → 管理踢线重连 → 断言回调仍为 1(去重),再手动 `Ack`;「已确认后再推则自动再 ack」仍由 FakeTransport 单测覆盖。
- 原因:自动模式下难以在不改服务器的前提下可靠丢掉已发出的 ack;确认超时默认约 5 分钟,不适合常规集成测。
- 备选:缩短测试用 ack_timeout(需改服务配置/代码,越界)。
- 影响:真实环境覆盖「不重复回调 + 终态确认」;自动再 ack 路径依赖既有单测。
3. **断线期间发送**
- 实际做法:管理 `POST .../kick`(AdministrativeAction,非 `0x8E`)断开连接,SDK 按网络故障重连;在重连窗口调用 `send` 入队,恢复后送达且回调一次。
- 原因:文档写明管理员踢下线只断线、令牌仍可用、SDK 应重连。
- 备选:本地 TCP 代理掐线;未采用以减少测试基础设施。
4. **JS 第 14 条跨源**
- 实际做法:另起本地 HTTP 端口作为「页面」Origin,对注册接口发带 `Origin` 的 OPTIONS/POST,断言 `Access-Control-Allow-Origin: *`;再用 SDK 从「页面」视角连服务器另一端口的 `/mqtt`。未起真实浏览器。
- 原因:Vitest/Node 无完整浏览器;服务端 CORS 与 WS Origin 策略已由身份/连接线保证。
- 备选:Playwright 实浏览器;本期为控制依赖未引入。
5. **任务 5 打包与文档**
- Go:`sdk/go/README.md` + `example/minimal`;不打 `sdk/go/v*` git tag(发布在 Z3)。
- JS:`README.md` + `example/minimal.mjs`;`license` 已为 `SEE LICENSE IN LICENSE`;`npm pack --dry-run` 试跑,**不** `npm publish`。
- 影响:无。
6. **真实 MQTT 收包路径与 request 死锁**
- 原条款:DEVELOPMENT 第 9 节收发/确认;回调串行。
- 实际做法:`resp` 在 MQTT `OnPublishReceived` 路径同步解挂起;`msg`/`receipt`/事件进入 `downCh` 由 `downLoop` 串行处理(可在其中 `request` 发 ack)。
- 原因:若在收包回调里同步 `request` 等 `resp`,而 `resp` 也走同一回调,真实 autopaho 会卡死;假传输因同栈注入 `resp` 掩盖了问题。
- 备选:ack 发后不等 `resp`;未采用,以免丢「ack 结果当 revoked」语义。
- 影响:行为更接近文档;单测仍绿。
## SDK 二 S2
### S2-PY/JAVA 1–3 2026-09-30
1. **HiveMQ「websocket 模块」用 netty-codec-http 显式依赖**
- 原条款:DEVELOPMENT 2.3「HiveMQ MQTT Client,加上 websocket 模块」。
- 实际做法:依赖 `com.hivemq:hivemq-mqtt-client:1.3.5`,并额外声明 `io.netty:netty-codec-http`;连接时用 `webSocketConfig().subprotocol("mqtt")`。未使用独立 artifact `hivemq-mqtt-client-websocket`(Maven Central 上该坐标未作为独立稳定模块发布)。
- 原因:与 HiveMQ 官方 WebSocket 用法一致,满足子协议 `mqtt`。
- 备选方案:若日后官方拆出独立 websocket 模块再改坐标。
- 影响:无行为差异。
2. **JSON 库选型**
- 原条款:未指定 Java JSON 库。
- 实际做法:Java 用 Gson(`disableHtmlEscaping`);Python 用标准库 `json`(`ensure_ascii=False`)。
- 原因:满足「不转义 HTML / 非 ASCII」;不引入过重依赖。
- 备选方案:Jackson。
- 影响:无。
3. **假传输单测,未接真实服务器**
- 原条款:任务 1–3 单元测试用假传输;任务 4 才做接入清单。
- 实际做法:Python `FakeTransport`、Java `FakeTransport` 覆盖 Clean Start、去重再 ack、本地超限、令牌回调、重交不改 `send_at_ms` / 消息号;未做对真实服务器的接入清单(任务 4)。
- 原因:本波范围。
- 备选方案:无。
- 影响:真实联调留待 S2 任务 4。
4. **Python 发布元数据**
- 原条款:`license = { file = "LICENSE" }` 与专有分类。
- 实际做法:`pyproject.toml` 已按此写;包内复制仓库根 `LICENSE`。未配置/执行 PyPI 发布。
- 原因:发布在阶段 3。
- 备选方案:无。
- 影响:无。
5. **Java 编译器用 JDK 21,目标字节码 8**
- 原条款:字节码目标 Java 8。
- 实际做法:`maven.compiler.release=8`,本机用 Temurin 21 编译。
- 原因:环境已有 JDK 21。
- 备选方案:用 JDK 8 工具链。
- 影响:无。
6. **Paho / HiveMQ 库内自动重连关闭,退避自管**
- 原条款:四种 SDK 同一套重连:1s 起加倍上限 30s ±30% 抖动,稳定 60s 恢复;每次 Clean Start、会话过期 0。
- 实际做法:两端均由 SDK 连接循环实现退避与停止条件;HiveMQ 不启库内 automaticReconnect;Paho 每次 `connect(..., clean_start=True)` 并设 `SessionExpiryInterval=0`。
- 原因:与 Go/JS 要求一致,避免两套重连。
- 备选方案:依赖库自带重连再改 Clean Start(易漏)。
- 影响:无。
### S2-PY/JAVA 4–5 2026-09-30
1. **接入清单对真实 nixmsg,跳过仅 JS 跨域**
- 原条款:DEVELOPMENT 第 9 节 15 条;任务 4 用 T0.5 启动器起真实服务端。
- 实际做法:Python `tests/harness.py` + `test_checklist.py`、Java `TestHarness` + `ChecklistTest` 自行 `go build`/`admin init`/`serve`(临时目录、`127.0.0.1:0`),管理登录后 `PUT /api/admin/registration` 开注册。覆盖清单 1–13、15;第 14 条(仅 JS 跨域)不做。
- 原因:总控指示跳过 JS 专属跨域;不改服务器业务代码。
- 备选方案:复用 Go `test/harness` 包(SDK 测试不便依赖)。
- 影响:无。
2. **清单第 4 条「ack 丢失后服务器重推」未在真机选择性复现**
- 原条款:模拟 ack 丢失后服务器重推,SDK 自动再确认且不重复回调。
- 实际做法:集成测验证同消息号防重与断线入队重交送达一次;「选择性丢弃 SDK 发出的 ack 帧」在真实 broker 上做不到,去重再 ack 仍由假传输单测覆盖。
- 原因:不改服务器、无中间代理注入丢包。
- 备选方案:toxiproxy 按包过滤(超出本任务、且难按 MQTT 应用帧过滤)。
- 影响:清单 4 真机为部分通过;假传输路径完整。
3. **下行 `resp` 与业务帧分流,避免 auto_ack 自死锁**
- 原条款:自动模式回调后发 ack;回调串行。
- 实际做法:MQTT/`publishes` 回调里对 `resp` 立即完成 pending;`msg` 等进单线程队列再处理(可在队列线程里同步 `ack`/`request`)。Paho 使用 `MQTTv5` 常量与 `transport=websockets`,并等待 SUBACK。
- 原因:若 `resp` 与 `msg` 同队列,auto_ack 等待 `resp` 会永久卡住。
- 备选方案:ack 只发布不等待(弱化协议确认)。
- 影响:与 DEVELOPMENT 行为一致,修复真机联调阻塞。
4. **HiveMQ 鉴权失败与顶号原因码解析**
- 原条款:CONNACK 鉴权失败停重连;`0x8E` 顶号停重连。
- 实际做法:`connect().get()` 抛出的 `Mqtt5ConnAckException` / 文案含 `BAD_USER_*` 时归为 `bad_credentials`(令牌场景 Client 层改为 `session_invalid`);断开原因从 `Mqtt5DisconnectException` 读 `SESSION_TAKEN_OVER`。`connectSync` 等到终态再返回,避免与 attemptConnect 竞态报 `busy/RECONNECTING`。
- 原因:HiveMQ 失败路径多为异常而非成功返回的 CONNACK 对象。
- 备选方案:无。
- 影响:无。
5. **任务 5:README/示例与打包试跑,不发布**
- 原条款:包名与许可证;工具试跑确认能打包;README 与最小示例;真正发布在 Z3。
- 实际做法:更新两端 README;Python `examples/minimal.py`;Java `asia.asio.nixmsg.examples.MinimalExample`;`python -m build` 产出 wheel/sdist;`mvn package -DskipTests` 产出 jar;`javap` major version 52(Java 8)。未上传 PyPI/Maven。
- 原因:本波范围。
- 备选方案:无。
- 影响:无。
## 测试交付 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 立即退出。
### Q2 第一部分(已合并功能验收)2026-09-30
1. **对真实进程探测,未接线则记「未测」而非改业务代码**
- 原条款:TASKS Q2「PRD 第 10 节每条至少有一个集成测试」;本波只做已合并功能的第一部分。
- 实际做法:`test/accept` 用 `test/harness`(随机端口 + 临时目录)起真实 `nixmsg`;对 `/api/admin/login`、`/api/client/register`、`POST /api/admin/endpoints` 先探测。404 则该条写「未测」并注明缺总控接线 / A2 / I 等,不修改 `cmd/nixmsg` 或业务包求绿。
- 原因:main@d357082 上 A1/I1 等仅为可挂载 Handler,`serve` 只挂了 `/healthz`、`/readyz`(见各线 DEVIATIONS「未改 cmd」)。
- 备选方案:测试进程内自行 `admin.New` 挂路由(不反映交付二进制行为,否决)。
- 影响:本波 F17/F01/F23 多为未测;接线后同一测试会自动跑登录锁定、CSRF、注册与开通路径。
2. **F22 只验收 init + 健康检查子集**
- 原条款:F22 含备份恢复、升级迁移、证书重载、Docker、指标。
- 实际做法:集成测试覆盖空目录 `admin init`、`serve`、`/healthz`、`/readyz`,并断言管理员密码不出现在 serve 的 stdout/stderr;其余 F22 子项仍标未测。
- 原因:本波范围是「现在就能测的路径」。
- 备选方案:本波强行跑 Docker/证书(超出第一部分)。
- 影响:对照表 F22 为「通过」但备注写明未覆盖项。
3. **验收报告写入方式**
- 原条款:用 `test/report` 生成 F01–F23 对照表。
- 实际做法:`TestQ2AcceptAndReport` 汇总探测结果;默认写临时目录。`task q:accept`(`NIXMSG_WRITE_ACCEPT_REPORT=1`)写入 `test/report/testdata/q2_results.json` 与 `test/accept/ACCEPTANCE.md`;`task q:report-q2` 可再生成 Markdown。普通 `go test`/`task check` 不改仓库文件。
- 原因:避免每次单测改 `generated_at` 弄脏工作区。
- 备选方案:固定时间戳始终写入仓库。
- 影响:交付审阅以 `ACCEPTANCE.md` / `q2_results.json` 为准,需先跑过 `task q:accept`。
### Q2 补齐 + Q3(本机 Windows)2026-09-30
1. **Q2 对照表按已接线能力重跑,不再把已挂路由写成未测**
- 原条款:TASKS Q2;PRD 第 10 节 F01–F23。
- 实际做法:在 `main@77d2dbd` 上重跑管理登录/CSRF/锁定、注册开关错码对码换码、开通端、单聊送达、延迟撤回、群发发送者不收到、离线保留上线送达、崩溃后续传;结果写入 `test/report/testdata/q2_results.json` 与 `ACCEPTANCE.md`。未覆盖项仍标未测并写明原因。不改业务逻辑求绿。
- 原因:L-WIRE / L-UPLINK / A3 / I5 已合入,旧报告过时。
- 备选方案:无。
- 影响:对照表通过项增加;未测项收窄到 SDK、拨钟类与部分身份/回执场景。
2. **Q3 弱网用 toxiproxy,不用本机 netem**
- 原条款:DEVELOPMENT 第 13 节 toxiproxy + Linux netem 20% 丢包。
- 实际做法:`test/chaos` 以项目名 `q3-chaos`、容器名 `q3-toxiproxy` 起官方镜像;注入延迟与 `reset_peer`,验证离线保留期内消息最终送达。Linux netem 20% 丢包记**未测**:本机 Windows,宿主无 tc;仅对代理容器挂 netshoot 不等于 NixMsg 端到端丢包验收。
- 原因:机器限制;TASKS 允许 Docker 内 Linux 测丢包,但本波未把业务进程放进同网络 Linux 容器做 netem。
- 备选方案:后续用 Linux 宿主或 compose 把 nixmsg 与 netem 旁路同网再测。
- 影响:丢包数字不进交付;延迟/断开路径有集成测。
3. **Q3 压测达不到 1000 连接 / 10 分钟**
- 原条款:PRD 第 8 节 / DEVELOPMENT 第 13 节「1000 连接、每秒 200 条、10 分钟」。
- 实际做法:`test/load` 短时 32 连接(16 对)真实登录+单聊收发成功;不宣称 1000/10min 通过。
- 原因:本机 Windows 开发机资源与并行 Agent 负载;强行 1000 长时间易误伤其他线。
- 备选方案:专用压测机或 Linux 服务器上再跑满指标。
- 影响:对照表与 DEVIATIONS 明示机器限制,不假装通过。
4. **崩溃续传用 accept.ManagedServer 启停**
- 原条款:提交成功后杀进程,重启后续传。
- 实际做法:`test/accept.ManagedServer`(Kill + 同 data_dir Restart)+ Q2/Q3 用例;不改 harness 公共 API(harness 属总控)。
- 原因:隔离目录约束。
- 备选方案:扩展 harness.Restart(需总控改)。
- 影响:Q 线自带启停辅助。