Files
NixMsg/docs/DEVIATIONS.md
T

74 KiB
Raw Blame History

实现与文档的偏差

开发中凡是实现和 PRD.md、DEVELOPMENT.md 不一致的地方,以及文档没写清、开发中自己拿主意的地方,都记在这里,交给负责人事后审阅。开发全程自动推进,记下来就继续,不等审阅。

每条写清:日期、原条款(文档和小节)、实际做法、原因、备选方案、影响。各线只写自己那一节,避免多条线同时改同一段。

总控 L

T0.1 2026-09-30

  1. 前端嵌入方式

    • 原条款:DEVELOPMENT 第 3 节 web/embed.go 用 //go:embed all:dist;TASKS T0.1;.gitignore 忽略 web/dist。
    • 实际做法:默认构建用 -tags 以外的 embed_stub.go 嵌入 web/stub/;task build 加 -tags embeddist 嵌入真实 web/dist。
    • 原因:web/dist 不进仓库,且要求没有前端产物时也能 go build。
    • 备选方案:提交最小 dist;或构建前脚本生成占位目录。
    • 影响:裸 go build 不含真实前端;正式产物必须走 task build。
  2. listen.addr 写入时机

    • 原条款:DEVELOPMENT 4.1「端口写 0 时」写 <data_dir>/listen.addr。
    • 实际做法:只要 serve 启动成功就写入实际监听地址。
    • 原因:测试启动器统一读取该文件更简单,固定端口场景也无害。
    • 备选方案:仅当配置端口为 0 时写入。
    • 影响:多一个小文件;行为超集,兼容文档要求。
  3. 迁移占位

    • 原条款:T0.3 才写完整表与 VACUUM INTO 备份;T0.1 允许空执行器加空 0001。
    • 实际做法:internal/store.Migrate 建 schema_migrations 并应用 0001_init.sql(内容为 SELECT 1;);不做迁移前备份。
    • 原因:保证 serve 可跑通迁移路径,表结构留给 T0.3。
    • 备选方案:完全空文件 + 只记版本。
    • 影响:T0.3 需替换 0001 正文并补备份逻辑;已应用的占位版本号仍为 1。
  4. Taskfile 引入 taskfiles

    • 原条款:TASKS T0.1 / 4.2 引入 taskfiles/*.yml。
    • 实际做法:includes: '*': taskfile: taskfiles/*.yml, optional: true,并放 _init.yml 占位。
    • 原因:空目录 glob 可能失败;各线稍后加自己的 yml。
    • 备选方案:主文件逐条 optional include 各线文件名。
    • 影响:无。
  5. deploy/Dockerfile

    • 原条款:完整多架构镜像属 Q4;T0.1 需要 task docker 目标。
    • 实际做法:提供单架构多阶段 Dockerfile 骨架,供 task docker 使用;T0.1 验证另用官方 golang 镜像跑 task check。
    • 原因:让 docker 目标可执行,又不抢 Q4 范围。
    • 备选方案:docker 目标仅 echo 提示。
    • 影响:镜像发布流程仍由 Q4 定稿。

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 始终写。
    • 影响:固定端口也会有地址文件。
  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,不进后端)。
  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

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 汇总页。
    • 影响:无。