# 实现与文档的偏差 开发中凡是实现和 [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 时」写 `/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 则写入 `/backup/pre-migrate-.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 `、位置参数,或从 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 线自带启停辅助。 ### Q4 定稿 + Q5 文档 2026-09-30 1. **多架构 buildx 本波不执行推送与双架构验证** - 原条款:DEVELOPMENT 11.4 / TASKS Q4「两个架构的镜像都能初始化、启动并通过健康检查」;`docker buildx` 一次推送 amd64+arm64。 - 实际做法:完善 Dockerfile(`TARGETOS`/`TARGETARCH`)、compose、`task q:docker-build` / `q:docker-push` / `q:docker-buildx`;本机只对当前架构(linux/amd64)`docker build` 并冒烟 `/healthz`;**不** `docker push`、**不**打版本 git 标签、**不**发布 SDK。本机 `docker buildx` 已列出 `linux/arm64`,但本波按总控指示不执行多架构构建与推送,留给 Z3。 - 原因:总控本波明确禁止真正 push / 打标签 / 发 SDK;双架构留给 Z3。 - 备选方案:在 Linux 宿主或已装 binfmt 的环境执行 `task q:docker-buildx`。 - 影响:交付标准「两架构镜像」与仓库推送仍待阶段 3。 2. **交叉编译三平台,本波至少验证 windows/amd64** - 原条款:DEVELOPMENT 11.2 三平台二进制。 - 实际做法:`task q:release-bins`(`CGO_ENABLED=0`)产出 `bin/nixmsg-linux-amd64`、`nixmsg-linux-arm64`、`nixmsg-windows-amd64.exe`;说明写入 README;二进制不提交。 - 原因:纯 Go + embed,交叉编译可行。 - 备选方案:仅本机 `task build`。 - 影响:发布物打包在 Z3。 3. **compose 容器/卷名带 q4 前缀** - 原条款:DEVELOPMENT 11.4 示例无固定 `container_name`;测试隔离要求名字带线前缀。 - 实际做法:`deploy/docker-compose.yml` 使用 `q4-nixmsg` / `q4-nixmsg-data`;正式部署可去掉 `container_name`。 - 原因:多 Agent 并行不抢容器名。 - 备选方案:compose 用项目名 `-p` 隔离而不写死 container_name。 - 影响:与文档示例略有差异,行为等价。 4. **验收未测项保持未测** - 原条款:交付标准要求 F01–F23 有结果;TASKS 本波 Q4/Q5 不做假装通过。 - 实际做法:`ACCEPTANCE.md` / `docs/OPS.md` 第 9 节明示 F03、F04、F07、F10、F11、F14、F15、F18、F19 仍为未测;不改对照表状态。 - 原因:本波范围是 Docker 定稿与文档。 - 备选方案:无。 - 影响:阶段 3 / 负责人审阅时须看到未测清单。 5. **Q5 文档落点** - 原条款:README、运维手册、SDK 文档汇总。 - 实际做法:重写根 `README.md`;新增 `docs/OPS.md`;SDK 汇总为 README 链到已有 `sdk/{go,js,python,java}/README.md`(各 SDK 已有最短使用说明,本波不重复扩写)。 - 原因:避免四份说明与 SDK 线漂移。 - 备选方案:在 docs/ 再建 SDK 汇总页。 - 影响:无。