88 KiB
实现与文档的偏差
开发中凡是实现和 PRD.md、DEVELOPMENT.md 不一致的地方,以及文档没写清、开发中自己拿主意的地方,都记在这里,交给负责人事后审阅。开发全程自动推进,记下来就继续,不等审阅。
每条写清:日期、原条款(文档和小节)、实际做法、原因、备选方案、影响。各线只写自己那一节,避免多条线同时改同一段。
总控 L
T0.1 2026-09-30
-
前端嵌入方式
- 原条款:DEVELOPMENT 第 3 节
web/embed.go用//go:embed all:dist;TASKS T0.1;.gitignore忽略web/dist。 - 实际做法:默认构建用
-tags以外的embed_stub.go嵌入web/stub/;task build加-tags embeddist嵌入真实web/dist。 - 原因:
web/dist不进仓库,且要求没有前端产物时也能go build。 - 备选方案:提交最小
dist;或构建前脚本生成占位目录。 - 影响:裸
go build不含真实前端;正式产物必须走task build。
- 原条款:DEVELOPMENT 第 3 节
-
listen.addr 写入时机
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写
<data_dir>/listen.addr。 - 实际做法:只要
serve启动成功就写入实际监听地址。 - 原因:测试启动器统一读取该文件更简单,固定端口场景也无害。
- 备选方案:仅当配置端口为 0 时写入。
- 影响:多一个小文件;行为超集,兼容文档要求。
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写
-
迁移占位
- 原条款:T0.3 才写完整表与
VACUUM INTO备份;T0.1 允许空执行器加空 0001。 - 实际做法:
internal/store.Migrate建schema_migrations并应用0001_init.sql(内容为SELECT 1;);不做迁移前备份。 - 原因:保证 serve 可跑通迁移路径,表结构留给 T0.3。
- 备选方案:完全空文件 + 只记版本。
- 影响:T0.3 需替换 0001 正文并补备份逻辑;已应用的占位版本号仍为 1。
- 原条款:T0.3 才写完整表与
-
Taskfile 引入 taskfiles
- 原条款:TASKS T0.1 / 4.2 引入
taskfiles/*.yml。 - 实际做法:
includes: '*': taskfile: taskfiles/*.yml, optional: true,并放_init.yml占位。 - 原因:空目录 glob 可能失败;各线稍后加自己的 yml。
- 备选方案:主文件逐条 optional include 各线文件名。
- 影响:无。
- 原条款:TASKS T0.1 / 4.2 引入
-
deploy/Dockerfile
- 原条款:完整多架构镜像属 Q4;T0.1 需要
task docker目标。 - 实际做法:提供单架构多阶段 Dockerfile 骨架,供
task docker使用;T0.1 验证另用官方golang镜像跑task check。 - 原因:让 docker 目标可执行,又不抢 Q4 范围。
- 备选方案:docker 目标仅 echo 提示。
- 影响:镜像发布流程仍由 Q4 定稿。
- 原条款:完整多架构镜像属 Q4;T0.1 需要
L-WIRE 2026-09-30
-
serve 真实接线范围
- 原条款:TASKS 总控接线;listener/broker/admin/注册/消息周期循环挂到进程。
- 实际做法:
cmd/nixmsg/serve.go注入真实auth.NewPool/NewSessionTokens/NewAPITokens/NewLoginLocks,挂注册与管理路由(踢线调Session.Kick),listener 识别 HTTP/WebSocket/OnMQTT裸 TCP,brokerLogin+Session,握手/断线/OnPublishDropped接到message.App,周期DispatchDue/PushPending/CleanupOnce,下行PublishDown。集成测覆盖:管理登录、写 settings 后注册、密码 MQTT 握手拿session_token。 - 原因:第二波收尾;三件验收必须通。
- 备选方案:分文件多阶段接线。
- 影响:进程可对外登录/注册/握手。
-
未接线 / 未完成部分(已被 L-UPLINK 部分取代)
- 原条款:上行应用帧完整分发到 identity/group/presence/message 业务方法。
- 实际做法(L-WIRE 当时):
Session处理 hello/logout;其余HandleUplink仍为 Stub。管理注册设置 HTTP(A3)未实现,测试直接写settings表。 - 原因:本波强制三件验收;完整协议分发属后续波次。
- 备选方案:本波同时实现 HandleUplink 大 multiplex。
- 影响:见 L-UPLINK。
-
listen.addr 始终写入
- 原条款:listener N1 仅端口 0 写文件;T0.1 超集为启动即写。
- 实际做法:listener 按 N1 写端口 0;serve 成功后再强制写一次
listen.addr(及分离时的admin.addr)。 - 原因:与 harness / T0.1 一致。
- 备选方案:改 listener 始终写。
- 影响:固定端口也会有地址文件。
L-UPLINK 2026-09-30
-
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.* 可走真实进程。
- 原条款:TASKS 总控接线;DEVELOPMENT 第 6 节上行帧由服务端处理后回
-
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)。
- 原条款:DEVELOPMENT 6.5「未知编号为 not_found」;
-
resp 超限改发 response_too_large
- 原条款:DEVELOPMENT 7.5「resp 超限改发 response_too_large」。
- 实际做法:发布前按连接表
MaxReceiveBytes与MaxPacketSize取较小正上限;超限则改发错误 resp(不再发原 data)。未做 MQTT 包头开销扣减(与 message 推送里的 overhead 预算不完全同一函数)。 - 原因:接线层最小可用检查。
- 备选方案:复用
message.effectivePayloadLimit(未导出)。 - 影响:接近包上限的大分页可能比推送路径略严或略松。
-
断线清 presence.watch
- 原条款:presence.watch 断线清空。
- 实际做法:
OnDisconnect额外调ClearWatch;SetOffline路径本身也会清。顶号旧连接未走SetOffline(isCurrent)时仍能清订阅。 - 原因:避免旧连接订阅泄漏。
- 备选方案:仅依赖 Session 对 isCurrent 调 SetOffline。
- 影响:无。
-
仍未接线 / 本波未覆盖
- 管理注册设置 HTTP(A3)仍未挂;集成测继续写
settings表开注册。 - 下行通知帧(
msg/receipt/revoked/presence/group_event/fatal)不是上行分发对象,由既有服务经 PublishDown 发出。 self.logout/hello仍在 Session,不经 appUplink。- 身份 I5 停用/删除级联、管理端群等非本任务范围。
- 集成测覆盖:双端单聊、离线 keep 后上线、群发不含发送者、延迟内撤回对方无回调;未穷尽 presence.watch 通知与全部 group.* 变体。
- 管理注册设置 HTTP(A3)仍未挂;集成测继续写
T0.2 2026-09-30
-
请求指纹规范化格式
- 原条款:DEVELOPMENT 7.3「下列字段规范化后的 SHA-256」,未规定字节布局。
- 实际做法:对
to.kind、to.id、body.enc、有效content_type、解码后正文、meta规范 JSON、send_at_ms、delay_ms、有效offline.keep/ttl_seconds、有效receipt做长度前缀(或有无标记)串联后算 SHA-256,输出小写十六进制;meta用encoding/json对map键排序序列化;不含talk_password、rid。 - 原因:文档未给规范格式,需固定、与键顺序无关、含可选字段区分。
- 备选方案:整段规范 JSON 对象再哈希。
- 影响:各语言 SDK / 服务端必须共用同一布局,否则防重失效。
-
指纹使用文档默认值后的有效字段
- 原条款:指纹字段列表含
receipt、offline.*,未说明缺省如何表示。 - 实际做法:
receipt缺省按 true;offline.keep缺省 false;keep为 true 且未给ttl_seconds时按 86400;keep为 false 时 ttl 记 0;content_type按 enc 补默认。 - 原因:重试时省略与显式默认应视为同一请求。
- 备选方案:按原始 JSON 有无字段区分,省略与显式默认算冲突。
- 影响:SDK 省略默认字段时防重仍命中。
- 原条款:指纹字段列表含
-
JSON Encoder 去掉尾部换行
- 原条款:用
json.Encoder且SetEscapeHTML(false)。 - 实际做法:Encode 后去掉
Encoder.Encode追加的\n,整帧字节数不含该换行。 - 原因:MQTT 一发布一帧,示例 JSON 无尾换行;保留换行会抬高帧长并与本地
frame_too_large判断不一致。 - 备选方案:保留换行并在 DEVELOPMENT 写明。
- 影响:线上帧比「裸 Encoder.Encode」少 1 字节。
- 原条款:用
-
协议包校验范围
- 原条款:T0.2 要求编号规则、正文/meta 大小、
send_at_ms/delay_ms互斥、登录密码不以nst_开头。 - 实际做法:上述必做之外,顺带校验各帧
v/type/rid、目标 kind、分页 limit、致命 reason 枚举等结构字段;业务错误(目标不存在、配额等)不在本包判定。 - 原因:无结构校验则编解码测试无法覆盖「合法帧」边界。
- 备选方案:协议包只做编解码,校验留给各 app 模块。
- 影响:服务端应复用本包
Validate,避免重复规则。
- 原条款:T0.2 要求编号规则、正文/meta 大小、
T0.3 2026-09-30
-
完整表放在 0002,不改已发布的 0001
- 原条款:TASKS T0.3 / 4.2「
0001_init.sql包含第 7.7 节全部表」;T0.1 偏差曾写「T0.3 需替换 0001 正文」。 - 实际做法:保留
0001_init.sql为SELECT 1;;新增0002_schema.sql写入 DEVELOPMENT 7.7 全部业务表与索引(含api_tokens、settings、session_hash等)。schema_migrations仍由迁移执行器CREATE TABLE IF NOT EXISTS维护,不放入 0002。 - 原因:T0.1 的 0001 可能已记入已有库的
schema_migrations;改写已发布迁移语义会导致「版本已应用但表不存在」。 - 备选方案:对未迁移库特殊检测并改写 0001(复杂且易错)。
- 影响:新库会有版本 1+2 两行;与 TASKS「表在 0001」字面不一致,与「不改已发布迁移」一致。
- 原条款:TASKS T0.3 / 4.2「
-
写入队列先做一操作一事务
- 原条款:DEVELOPMENT 7.2 合并提交(最多 256 或凑满 2ms,SAVEPOINT);TASKS T0.3 允许简单实现,P2 换合并。
- 实际做法:
store.Queue用互斥锁串行,每请求一个事务;注释与本条标明 P2 再改为写 goroutine 合并提交。 - 原因:本任务范围;合并留给平台 P2。
- 备选方案:T0.3 直接做合并(抢 P2 范围)。
- 影响:高并发写入落盘次数偏多,正式压测前需完成 P2。
-
空库不备份;仅已有 db 文件且有未应用版本时 VACUUM INTO
- 原条款:DEVELOPMENT 7.7「有未应用版本时先 VACUUM INTO」;未区分空库。
- 实际做法:
Open在打开前检查nixmsg.db是否已存在;不存在则跳过备份;存在且有 pending 则写入<data_dir>/backup/pre-migrate-<UTC时间>.db。迁移失败返回错误,不自动从备份恢复。 - 原因:空库备份无意义;失败退出与文档一致,恢复交给运维。
- 备选方案:失败时自动还原备份再退出。
- 影响:与任务说明一致;运维需知备份路径。
T0.5 2026-09-30
-
admin init / 管理员密码
- 原条款:TASKS T0.5「自动执行
admin init拿到管理员密码」;命令行启动器 JSON 含管理员密码。 - 实际做法:定义
AdminInitializer(默认CLIAdminInit);二进制尚无admin子命令时返回ErrAdminInitUnsupported,启动器跳过并继续起 serve;admin_password字段为空字符串。示例集成测试只验/healthz。 - 原因:
admin init属 P1,当前 main 仅有version/serve。 - 备选方案:harness 内嵌假密码写入库(无表结构可写);或阻塞等 P1。
- 影响:P1 合入后无需改调用方接口,密码解析约定见
parseAdminPassword;Q/SDK 集成测试在拿到非空密码前勿依赖管理登录。
- 原条款:TASKS T0.5「自动执行
-
MQTT 测试客户端范围
- 原条款:能收发 DEVELOPMENT 第 6 节应用帧。
- 实际做法:提供 TCP / WebSocket(
/mqtt,子协议mqtt)传输层MQTTClient,收发原始 MQTT 控制包字节;不实现 CONNECT/hello/主题业务。 - 原因:内置 broker 与协议处理尚未合入(连接 N / 后续任务);T0.5 先给可连传输与占位 API。
- 备选方案:引入完整 MQTT 客户端库并编假 broker。
- 影响:业务级帧测试在 broker 可用后由各线基于
Send/Recv或再包一层完成。
-
进程停止方式
- 原条款:优雅停机(DEVELOPMENT 7.8 / P1)。
- 实际做法:测试启动器对子进程使用
Kill(Windows 上Interrupt不可靠)。 - 原因:保证并行测试与清理在 Windows 上稳定。
- 备选方案:Unix 发 SIGTERM;Windows 用 Job Object / Ctrl+Break。
- 影响:不覆盖优雅停机验收;该验收仍归 P1/Q。
T0.4 2026-09-30
-
broker↔app 契约包放在
internal/app/port- 原条款:TASKS T0.4「broker 和 app 之间的接口」;示例路径
internal/app/port或internal/broker/port。 - 实际做法:放在
internal/app/port:UplinkHandler(broker→app)、Downlink/ConnControl(app→broker)。不依赖 mochi。 - 原因:契约由 app 消费形态主导,避免 broker 包在 N 线实现前成为空壳;N 线实现 broker 时 import 本包即可。
- 备选方案:放在
internal/broker/port或单独internal/port。 - 影响:N/M/I 依赖路径固定为
internal/app/port。
- 原条款:TASKS T0.4「broker 和 app 之间的接口」;示例路径
-
接口方法先返回未实现或空操作,不做业务状态机
- 原条款:T0.4 要求 Go 接口与测试假实现;不要实现真正业务逻辑。
- 实际做法:
auth/message/identity/group的写路径假实现返回ErrNotImplemented;调度/推送/清理/在线查询等返回空成功或固定假数据;wire()组装这些假实现,serve仅调用RecoverOnStart(空操作)并保留依赖引用。 - 原因:让后续各线有可编译的替换点,且不抢 P/N/M/I/A 实现范围。
- 备选方案:接口方法全部 panic;或完全不接线 serve。
- 影响:在假实现替换前,端协议与管理 API 仍不可用(本任务预期)。
-
管理契约补充
GET /api/admin/groups/{id}- 原条款:DEVELOPMENT 第 8 节路由表列出 groups 的 GET/POST 列表创建与 PATCH/DELETE,未单列群详情。
- 实际做法:
docs/api/admin-api.md增加GET /api/admin/groups/{id}(成员分页),供后台详情页使用。 - 原因:改名/解散/成员管理需要详情;与端协议
group.get对称。 - 备选方案:详情拼进列表项或仅用 PATCH 回显。
- 影响:A/W 按契约实现该只读路由。
-
CSV 导入校验失败时用信封外的
data.errors- 原条款:写明返回出错行号和原因;未规定 JSON 形状。
- 实际做法:HTTP 400,
ok=false,error.code=bad_request,同行号列表放在顶层data.errors。 - 原因:通用
error只有 code/message,放不下多行明细。 - 备选方案:把明细塞进
error.message字符串。 - 影响:W 线按
data.errors渲染。
平台 P
P1 2026-09-30
-
admin set-password 传参方式
- 原条款:DEVELOPMENT 11.2 仅列命令名,未规定密码如何传入。
- 实际做法:支持
--password <pwd>、位置参数,或从 stdin 读一行;最短 12 位。 - 原因:自动化测试与 Docker 非交互环境需要非交互传参。
- 备选方案:仅交互式 prompt。
- 影响:运维文档需写明推荐用
--password或管道,勿把密码写进 shell 历史时可改用 stdin。
-
admin init 密码字符集
- 原条款:生成 20 位密码,未规定字符集。
- 实际做法:从去掉易混字符(0/O/1/I/l)的字母数字中均匀抽样 20 位。
- 原因:终端抄写友好。
- 备选方案:全 ASCII 可打印字符。
- 影响:熵略低于全字符集,对 20 位仍足够。
-
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 无并发压力)。
-
优雅停机顺序
- 原条款:停接受 → 写队列最多 10 秒 → 断开连接退出。
- 实际做法:
http.Server.Shutdown(先停接受并等待进行中的 HTTP)后,再Queue.Drain最多 10 秒;本期尚无长连接表,断开连接由 Shutdown 覆盖。 - 原因:当前 main 仅有 HTTP 健康检查;N 线接入后应在 Drain 前后显式踢连接。
- 备选方案:先关 listener、Drain、再 Shutdown。
- 影响:有 MQTT 长连接后需 N/P 联调停机路径。
P2 2026-09-30
-
合并写入队列替换 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。
-
调用方 context 取消与已入队任务
- 原条款:未规定入队后取消。
- 实际做法:入队前检查 ctx;批次执行前再检查;若调用方在等待结果时取消,最多再等 30 秒取结果以免泄漏。
- 原因:写 goroutine 仍可能已执行该操作,不能静默丢结果。
- 备选方案:取消即从队列摘除(需可取消数据结构)。
- 影响:极端取消场景下调用方可能多等一会儿。
P3 2026-09-30
-
管理员/注册锁定阈值沿用登录 IP 档
- 原条款:登录/对话密码阈值写清;管理员登录与注册安全码仅写「临时锁定」,未给数字。
- 实际做法:
LockAdminIP、LockRegisterIP、LockTalkPair/LockLoginEndpointIP均为 5 分钟窗口 10 次、锁 5 分钟;LockTalkTarget/LockLoginEndpoint为 1 小时 50 次、锁 1 小时。 - 原因:与 PRD D18/F23「默认 5 分钟 10 次」叙述一致。
- 备选方案:管理员单独更严阈值。
- 影响:A/I/N 线直接用
LoginLocks即可。
-
真实 auth 实现未改 wire.go
- 原条款:平台不改
wire.go;P3 实现接口。 - 实际做法:提供
NewPool/NewSessionTokens/NewAPITokens/NewLoginLocks;wire()仍用 Stub,由总控或各线接线时替换。 - 原因:分工禁止改 wire.go。
- 备选方案:在 serve 旁路替换(会绕过 wire)。
- 影响:合入后需有一次接线才能在进程内用上真实哈希池。
- 原条款:平台不改
-
令牌随机部分用 RawURLEncoding
- 原条款:32 字节随机数的 base64url。
- 实际做法:
encoding/base64.RawURLEncoding(无 padding)。 - 原因:URL/Header 友好,与常见 token 惯例一致。
- 备选方案:StdEncoding 带 padding。
- 影响:SDK/文档示例需无
=结尾。
P4 2026-09-30
-
指标包放在
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。
-
指标命名
- 原条款:列了指标含义,未规定 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
-
未接线
cmd/nixmsg- 原条款:serve 最终应挂上端口识别、broker、
/mqtt。 - 实际做法:本任务只交付
internal/listener、internal/broker;按总控要求不改cmd/nixmsg。 - 原因:避免与平台/总控并行改 wire 冲突;合并时再接线。
- 备选方案:本分支顺带改
wire.go(与指令冲突)。 - 影响:当前
serve仍是 T0.4 的简单/healthz监听,不含 MQTT。
- 原条款:serve 最终应挂上端口识别、broker、
-
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。
-
登录校验为可替换接口,默认拒绝
- 原条款:第 5 节完整会话令牌/密码/锁定属 N3。
- 实际做法:
broker.Authenticator接口 + 默认RejectAuthenticator;内部错误在OnConnect返回 error;测试提供AllowAuthenticator。 - 原因:N3 范围;N2 需可跑通装配与钩子。
- 备选方案:N2 内做假登录表(超出范围)。
- 影响:真实端连不上直到 N3;总控接线时注入 Authenticator。
-
大帧并发名额释放策略
- 原条款:DEVELOPMENT 7.5 大于 64KiB 全局同时不超过 64;PUBACK / 超时 / 断线释放。
- 实际做法:发布前申请名额;QoS 0 发布成功立即释放;QoS 1 在
OnQosComplete且 payload>64KiB 时释放,断线releaseAllLarge;未单独做「确认超时」计时释放(确认超时属 M 线推送循环)。 - 原因:N2 无投递确认计时器;与 M 线推送超时释放衔接。
- 备选方案:broker 内对大帧自建超时(与 M 重复)。
- 影响:若客户端永不 PUBACK 且不断线,名额可能占满直到断开;M 线超时踢线或回调 Disconnect 可释放。
-
OnPublishDropped仅打日志- 原条款:清「已推送」标记并 1 秒后重推。
- 实际做法:钩子记录 debug 日志;清标记/重推留给消息 M。
- 原因:投递状态在 M/store,N2 无投递表。
- 备选方案:N2 暴露回调给 M 注册。
- 影响:接线后 M 需订阅或包装该钩子;当前接口可后续加
OnPublishDropped回调字段。
N3 2026-09-30
-
仍未接线
cmd/nixmsg- 原条款:serve 最终应挂上真实 Authenticator / Session。
- 实际做法:交付
broker.Login、broker.Session与 F02 测试;不改cmd/nixmsg/wire.go。 - 原因:与总控/其他线并行改 wire 冲突;N1/N2 已约定合并时接线。
- 备选方案:本分支改 wire(与隔离指令冲突)。
- 影响:进程默认仍 RejectAuthenticator,需接线注入
Login+Session。
-
session_hash存十六进制文本- 原条款:库中存 SHA-256;列为 TEXT,未规定编码。
- 实际做法:存 32 字节哈希的小写 hex(与后台 API 令牌存法一致)。
- 原因:TEXT 列无法直接存原始字节;hex 便于排查。
- 备选方案:BLOB 列或 base64。
- 影响:其他线读写
session_hash需按 hex 编解码。
-
上下线通知走
PresenceSink+ port 回调- 原条款:写
online_since/offline_since并通知;通过现有 port 接口供身份线订阅。 - 实际做法:N3 自己写时间戳;可选注入
PresenceSink(对齐presence.Service.SetOnline/SetOffline);并继续调用OnHandshakeComplete/OnDisconnect。旧连接断开用连接代号判断,只有当时仍是 current 才标离线。 - 原因:I3 尚未合入,不能依赖具体 presence 实现;双通道便于接线。
- 备选方案:只靠 port、由 I 线写库(与「N3 写 online_since」字面不符)。
- 影响:接线时避免 I 线重复写同一时间戳即可。
- 原条款:写
-
InlineClient 的
OnPublish必须放行- 原条款:客户端上行
OnPublish返回CodeSuccessIgnore。 - 实际做法:
cl.Net.Inline时原样返回,不 Ignore,否则PublishDown无法送达订阅者。 - 原因:mochi
Publish经 InlineClientInjectPacket再进OnPublish。 - 备选方案:不用 InlineClient,改直接
publishToClient(偏离文档装配)。 - 影响:N2 既有 PublishDown 测试此前未读回包,此缺陷在 N3 才暴露并修复。
- 原条款:客户端上行
-
管理员踢线类入口挂在
Session- 原条款:停用/删除/重置密码先 fatal 再断开;踢下线只断开。
- 实际做法:
Session.Disable/Deleted/ResetPassword/Kick可调用;管理 HTTP 未接。 - 原因:A2 管理接口尚未接线。
- 备选方案:放到
internal/admin(超出 N 目录)。 - 影响:A/I 接线时调用这些方法即可。
消息 M
M1 2026-09-30
-
提交时分发做成最小正确版(已被 M2 取代)
- 原条款:DEVELOPMENT 7.3 步骤 8 / 7.4:
send_at已到则同一写操作内完整分发。 - 实际做法(M1):单聊/群插
pending后改dispatched,不做停用/配额/宽限。 - 现状(M2):
Submit到点与DispatchDue均走完整dispatchFullTx(7.4)。 - 原因 / 备选 / 影响:见 M2。
- 原条款:DEVELOPMENT 7.3 步骤 8 / 7.4:
-
请求频率突发容量写死为 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 线也限流可能双重计数。
- 原条款:DEVELOPMENT 6.10 每端每秒 50、突发 100;配置示例仅有
-
未接线
cmd/nixmsg- 原条款:可替换 T0.4 假实现。
- 实际做法:
message.App实现 Service;保留Stub;未改cmd/nixmsg/wire.go。 - 原因:本任务隔离;总控接线。
- 备选方案:本任务直接改
wire.go。 - 影响:进程内仍用 Stub,需显式
message.New并注入Downlink/ConnRegistry。
-
防重键在、消息行已删时返回
not_found- 原条款:防重命中返回原消息当前状态;未写明消息行已被清理时的提交重试行为。
- 实际做法:
send_keys指纹相同但messages无行时返回not_found。 - 原因:无法构造
send_at/state。 - 备选方案:在
send_keys冗余存结果快照。 - 影响:保留天数 0 完成后重试不再幂等成功(与 F18 防重「记录还在时」一致)。
M2 / M3 / M4 2026-09-30
-
下行与在线用可注入接口,测试用假实现
- 原条款:推送经 broker
Downlink;连接表在 N 线内存。 - 实际做法:
WithDownlink/WithConnRegistry;测试用RecordingDownlink、MemoryConns。状态机全在message包。未接真实 MQTT/mochi。 - 原因:N3 握手与 wire 本波未强制合入;任务允许假下行。
- 备选方案:直接依赖
internal/broker.Broker。 - 影响:接线方需在握手/断线时调用
OnHandshakeComplete/OnDisconnect,登记连接,并把OnPublishDropped转到App。
- 原条款:推送经 broker
-
大帧并发名额在 message 包再管一份
- 原条款:大于 64KiB 全局同时不超过 64(DEVELOPMENT 7.5);N2 broker 已有信号量。
- 实际做法:
App内另有容量 64 的largeSem,发布前申请,确认/超时/清标记时释放。 - 原因:假
Downlink不经 broker 时仍要满足上限。 - 备选方案:只依赖 broker,测试也走真实 PublishDown。
- 影响:接线真实 broker 后可能双重限流(更严,不破坏语义)。
-
确认超时按库内
pushed_at判定,不另开每连接计时器 goroutine- 原条款:推送循环在内存里计时。
- 实际做法:
PushPending开头扫描该连接已推且now - pushed_at >= ack_timeout的投递,再按 keep/expire 规则处理。 - 原因:与崩溃恢复一致、测试可拨钟;避免无调度器时泄漏计时器。
- 备选方案:每连接
time.AfterFunc。 - 影响:需周期性调用
PushPending(或WakePush)才会触发超时。
-
后台调度/清理循环未在 App 内自启
- 原条款:调度按
send_at唤醒;清理约每秒;推送每连接一循环。 - 实际做法:导出
DispatchDue、PushPending、CleanupOnce、RecoverOnStart、WakePush;由接线方起 goroutine。WakePush在有连接时异步PushPending。 - 原因:未改
cmd/nixmsg;避免无 context 的后台泄漏。 - 备选方案:
App.Start(ctx)内启三循环。 - 影响:未接线则定时消息不会自动到点,需外部调用
DispatchDue。
- 原条款:调度按
-
回执推送窗口未单独记 inflight
- 原条款:回执窗口默认 64,确认一笔再推下一笔。
- 实际做法:按
acked=0取最多ReceiptWindow条尽力发布;不因未receipt_ack停推后续。 - 原因:简化;回执可重复、SDK 按
receipt_id去重。 - 备选方案:内存记已推未确认回执数。
- 影响:发送方慢确认时可能多推几条回执(协议允许重复)。
-
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
-
注册做成可挂载 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 接线才对外可访问。
- 原条款:TASKS I1 / DEVELOPMENT 6.9 在
-
密码哈希与锁定走 auth 接口,本分支用可替换假实现测
- 原条款:依赖 P3 argon2 池与锁定计数器。
- 实际做法:
RegisterConfig.Hash/Locks注入auth.HashPool、auth.LoginLocks;测试用auth.NewStubHashPool+ 仅实现LockRegisterIP(5 分钟 10 次)的测试锁定器,不在本线重写 argon2。 - 原因:P3 尚未在本分支。
- 备选方案:等 P3 合入后再写 I1。
- 影响:生产须注入 P3 实现;StubLoginLocks 永不锁定,不能直接用于开放注册。
-
settings 开关取值
- 原条款:
settings.registration_enabled,未规定字符串字面量。 - 实际做法:
1/true/yes/on(大小写不敏感)视为开启,其余(含缺省)关闭;安全码键registration_code。 - 原因:与 store 测试写入的
"0"/"1"对齐并兼容常见布尔字面量。 - 备选方案:仅认
"1"。 - 影响:A 线写注册设置时宜写
"1"/"0"。
- 原条款:
-
客户端 IP
- 原条款:DEVELOPMENT 4.5 受信任代理下用
X-Forwarded-For。 - 实际做法:Handler 默认取
RemoteAddr的 host;可通过RegisterConfig.ClientIP注入。本线不做trusted_proxies解析(属 listener/接线)。 - 原因:不改 listener;代理 IP 应由外层在挂载前算好或注入。
- 备选方案:在 identity 内复制 4.5 逻辑。
- 影响:经代理部署时接线方必须注入真实 IP,否则锁定按直连 IP 计。
- 原条款:DEVELOPMENT 4.5 受信任代理下用
-
生成登录密码长度
- 原条款:F01 留空则生成,8–128 字符,不以
nst_开头;未规定生成长度。 - 实际做法:生成 20 位字母数字;若偶然以
nst_开头则重抽。 - 原因:与管理员 init 量级接近,满足规则。
- 备选方案:16/32 位。
- 影响:无产品行为差异。
- 原条款:F01 留空则生成,8–128 字符,不以
I2 / I3 / I4 2026-09-30
-
服务方法可直接调用,未接 MQTT 分发
- 原条款:端协议帧经 broker 上行分发到 app。
- 实际做法:
identity.App/presence.App/group.App实现 Service 方法;单测直接调用,不经 mochi。cmd/nixmsg/wire.go未改。 - 原因:任务要求可被协议分发调用的服务方法 + 直接调用验证;N3 连接事件与总控接线另波。
- 备选方案:本分支顺带改 wire(与隔离冲突)。
- 影响:合入后需总控/N 接线 HandleUplink → 各 Service;MQTT 帧路径未测。
-
在线状态:库字段 + 可注入连接表 + 本包握手表
- 原条款:在线以真实连接为准;依赖 N3 连接事件。
- 实际做法:
presence.ConnTable可注入;另用SetOnline/SetOffline维护内存表并写endpoints.online_since/offline_since;查询优先 ConnTable,其次内存表,再回退库字段(online_since晚于offline_since或后者为空)。 - 原因:N3 可能尚未合入,不阻塞 I3。
- 备选方案:阻塞等 N3。
- 影响:未接线时须调用 SetOnline/SetOffline 或写库字段;拔网线心跳超时属 N 线,本波单测不覆盖。
-
presence / group_event 经 Downlink QoS 0,可 nil
- 原条款:上下线与群事件尽力推送、不落库。
- 实际做法:注入
port.Downlink时编码帧并PublishDown(presence/group_event QoS 0;退群已推送投递的revoked用 QoS 1);Downlink 为 nil 时跳过推送,业务库操作仍完成。 - 原因:无 MQTT 时仍可测库逻辑。
- 备选方案:强制假 Downlink。
- 影响:接线后必须注入真实 Downlink 才有通知。
-
self.logout 踢线可选
- 原条款:回 resp 后断开连接。
- 实际做法:清
session_hash;若注入port.ConnControl则Disconnect,否则仅清令牌。 - 原因:未接 broker。
- 备选方案:无。
- 影响:接线方应注入 ConnControl。
-
session_hash 存 SHA-256 十六进制
- 原条款:库中存会话令牌 SHA-256。
- 实际做法:
hex.EncodeToString(hash)写入 TEXT 列。 - 原因:文档未规定编码;十六进制便于调试与比对。
- 备选方案:BLOB/Base64。
- 影响:N3 校验须用同一编码。
-
self.update 空 name 不写库
- 原条款:可更新 name。
- 实际做法:
name非空才 UPDATE name;仅改default_delay_ms时不碰 name(JSON omitempty 无法区分省略与空串)。 - 原因:避免误清空名称。
- 备选方案:用指针字段区分。
- 影响:端无法通过协议把名称改成空字符串(可用空格等)。
-
进群密码校验不产生单聊授权
- 原条款:拉人须当次带密码;已有授权不能代替。
- 实际做法:
CheckTalkPasswordForJoin只校验,不写talk_grants。 - 原因:与 F15「进群仍要密码」一致,避免进群副作用放宽单聊。
- 备选方案:校验成功顺带写 password 授权。
- 影响:仅进群成功后,单聊仍须 unlock/发送带密。
-
群作废在 group 包内写 deliveries/messages
- 原条款:退群/踢人/解散的投递作废属 7.6,消息线亦相关。
- 实际做法:I4 在
group写操作里直接改pending→rejected、scheduled→completed/group_dissolved,删正文行,需要时插消息级回执,已推送则经 Downlink 发revoked。 - 原因:I4 验收依赖作废规则;M 线完整推送循环可能未合入。
- 备选方案:只调 message 钩子(接口尚未暴露)。
- 影响:与后续 M 作废路径需保持同语义,避免重复作废。
-
UnlockTalk / SelfChangeLoginPassword / CheckTalkPasswordForJoin 增加 remoteIP
- 原条款:锁定按发送方+对方 / 编号+IP。
- 实际做法:Service 方法增加
remoteIP参数供锁定计数;T0.4 Stub 同步改签名。 - 原因:无 ConnInfo 的直接调用测试需要显式 IP。
- 备选方案:塞进 context。
- 影响:协议分发接线时从
ConnInfo.RemoteIP传入。
-
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
-
停用/删除级联在 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),否则停用/删除仍无完整作废。
-
消息级作废回执 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。
- 原条款:DEVELOPMENT 6.4 消息级作废写
-
删除时群主转让按
joined_at最早,并列按编号- 原条款:转给最早加入的其他成员。
- 实际做法:
ORDER BY joined_at ASC, endpoint_id ASC LIMIT 1。 - 原因:同时加入时需稳定次序。
- 备选方案:仅按 joined_at。
- 影响:同毫秒加入时编号小者优先。
后台接口 A
A1 2026-09-30
-
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。
-
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可保留作测试替身。
- 原条款:密码与令牌哈希用
-
API 令牌
id为字符串- 原条款:
docs/api/admin-api.md示例"id": 1(数字)。 - 实际做法:遵循库表
api_tokens.id TEXT,响应id为 16 字节随机十六进制字符串。 - 原因:不改已发布迁移;与 schema 一致。
- 备选方案:另加 INTEGER 列或把数字存成文本并在 JSON 里发数字。
- 影响:W 线类型应按
string解析令牌 id。
- 原条款:
-
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 前勿依赖尚未实现的业务响应。
-
操作日志用
slog结构化字段- 原条款:写结构化日志(操作者、动作、对象、结果、来源 IP)。
- 实际做法:
Logger.Info("admin_audit", "actor", ..., "action", ..., "object", ..., "result", ..., "ip", ...);改状态请求写日志;不写密码/令牌/正文。 - 原因:文档未规定日志后端。
- 备选方案:独立 audit 表。
- 影响:日志采集需按 msg=
admin_audit过滤。
A2 2026-09-30
-
停用/删除完整级联留给 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(单元测试可注入)。
-
在线状态读库字段,不依赖 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 写入前,列表会显示离线。
-
login_locked仅反映按编号的登录锁定- 原条款:列表含
login_locked。 - 实际做法:查
LockLoginEndpoint;不枚举「编号+IP」锁定。unlock仍调用ClearEndpoint清两种。 - 原因:
LoginLocks接口无「是否任一 IP 锁定」查询。 - 备选方案:扩展 locks 接口。
- 影响:仅 IP 档锁定时列表可能仍显示未锁定,但 unlock 可解除。
- 原条款:列表含
-
A2 时未挂载到
cmd/nixmsg;L-WIRE / A3 已接线- A2 当时:可挂载 Handler;接线与
KickEndpoint注入留给总控。 - 现状:
serve已挂载 Admin Handler;A3 起注入Groups/Config/Version/KickEndpoint。 - 影响:无。
- A2 当时:可挂载 Handler;接线与
A3 2026-09-30
-
概览增加
endpoints_self(自助注册数)- 原条款:PRD F17「端数量(其中自助注册的数量)」;
admin-api.md示例未列该字段。 - 实际做法:
GET /api/admin/overview同时返回契约字段与endpoints_self(source='self'计数)。 - 原因:以 PRD / 本任务说明为准补齐。
- 备选方案:只返回 api 文档字段,自助数由前端筛端列表。
- 影响:W 线可选用该字段;旧 mock 类型可增补。
- 原条款:PRD F17「端数量(其中自助注册的数量)」;
-
群列表/详情直接读库;变更走
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。
- 原条款:群操作调用
-
后台建群不接受自定义
id- 原条款:admin-api「id 留空则生成」。
- 实际做法:
AdminCreate无自定义 id 参数;请求带非空 id 返回400。 - 原因:不改身份线接口签名。
- 备选方案:扩展
AdminCreate接受可选 id。 - 影响:后台只能服务器生成群编号。
-
/metrics门禁抽到internal/httpx.MetricsGate- 原条款:A 线负责 metrics 访问规则(DEVELOPMENT 4.3)。
- 实际做法:规则实现放
httpx.MetricsGate;listener.NewMuxRoleShared 调用之(N 目录一行替换)。单独后台监听仍直接挂 Handler 不鉴权。 - 原因:A 负责目录是
admin+httpx;listener 仅接线。 - 备选方案:门禁留在 listener。
- 影响:
serve已传MetricsToken;共用端口无令牌 404、错令牌 401。
-
注册安全码写入审计不含明文
- 原条款:安全码明文返回已登录管理员,不写日志。
- 实际做法:GET/PUT 响应含
code;admin_audit的 object 为空,不记安全码。存库键registration_enabled=1/0,与 I1 一致。 - 原因:D14 / DEVELOPMENT 12 节。
- 备选:无。
- 影响:无。
-
消息查询不碰
message_bodies- 原条款:只读 messages 与 deliveries;响应无正文。
- 实际做法:SELECT 不含
body/body_enc以外的正文列(messages 仍有body_enc列但查询不选它);不 JOINmessage_bodies。 - 原因:任务硬性要求。
- 备选:无。
- 影响:无。
后台网页 W
-
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仍用假数据。登录页假数据提示已去掉。
-
列表分页用页码映射 cursor 偏移
- 原条款:admin-api 使用
cursor/limit游标分页。 - 实际做法:假数据把
page编成数字偏移 cursor(String((page-1)*limit)),n-data-tableremote 分页照常。 - 原因:Naive UI 表格以页码交互;契约游标对前端透明即可。
- 备选:W4 若后端 cursor 非偏移编码,在
admin.ts内适配,页面仍用页码。 - W4:后端 cursor 为不透明字符串;首页(空 cursor)主路径 e2e 通过。翻到第 2 页若用数字偏移可能无效,未改页面交互;后续若要稳定翻页,应在
admin.ts缓存服务端next_cursor。
- 原条款:admin-api 使用
-
帮助图标不用独立图标库
- 原条款:DEVELOPMENT 2.4 用
n-tooltip;协作规则要求只用 Naive UI。 - 实际做法:
HelpTip用带边框的?文字触发 tooltip,不引入@vicons/*。 - 原因:避免第二套依赖;视觉已够用。
- 备选:若设计要求统一图标,可只加
@vicons/ionicons5作图标资源(仍非第二套组件库)。
- 原条款:DEVELOPMENT 2.4 用
-
操作日志页本期未做
- 原条款:PRD F17 含管理操作日志;admin-api 写明改状态请求记日志,但未单列日志查询路由。
- 实际做法:W3 菜单不含操作日志页。
- 原因:契约无列表接口,无法按契约做假数据页。
- 备选:A 线补查询接口后 W4/后续补页。
-
端「踢下线」放在行内操作,未单独成页
- 原条款:PRD F17 含踢下线。
- 实际做法:端列表行操作调用
POST .../kick。 - 原因:与重置密码、解锁同级的行内动作更贴桌面工作流。
- 备选:无。
W4 2026-09-30
-
接真实接口并适配 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;页面逻辑不变。
- 原条款:TASKS W4;admin-api 令牌
-
未完成投递由 e2e 用 MQTT seed 造出
- 原条款:DEVELOPMENT 第 13 节后台主路径「看到未完成记录」;管理 messages 只读。
- 实际做法:Playwright 流程里用管理接口开通第二端后,运行
web/e2e/seedpending(MQTT hello + 未来send_at_ms的 send),再打开投递记录页断言scheduled。测完杀进程并删临时目录。 - 原因:管理 API 不能写消息;不改服务器业务代码。
- 备选:插入 SQLite(并发/锁风险);或依赖已有记录(空库无)。
- 影响:e2e 依赖本机
go与 Chromium。
-
Playwright 挂入
task itest/task w:e2e- 原条款:TASKS W4「端到端测试在 task itest 里通过」;Taskfile 由总控维护、各线写
taskfiles/<线>.yml。 - 实际做法:新增
taskfiles/w.yml;主Taskfile.yml显式includes.w(本机 Task 3.53 下taskfiles/*.ymlglob 未挂上各线任务)并把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。
- 原条款:TASKS W4「端到端测试在 task itest 里通过」;Taskfile 由总控维护、各线写
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 接入清单与打包文档
-
集成测试自建启动器(不 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 一致,仅实现重复约百行。
-
清单第 4 条「ack 丢失后自动再确认」
- 原条款:模拟 ack 丢失后服务器重推,SDK 自动再确认且不重复回调。
- 实际做法:真实服务用
ManualAck+keep消息:收一次不 ack → 管理踢线重连 → 断言回调仍为 1(去重),再手动Ack;「已确认后再推则自动再 ack」仍由 FakeTransport 单测覆盖。 - 原因:自动模式下难以在不改服务器的前提下可靠丢掉已发出的 ack;确认超时默认约 5 分钟,不适合常规集成测。
- 备选:缩短测试用 ack_timeout(需改服务配置/代码,越界)。
- 影响:真实环境覆盖「不重复回调 + 终态确认」;自动再 ack 路径依赖既有单测。
-
断线期间发送
- 实际做法:管理
POST .../kick(AdministrativeAction,非0x8E)断开连接,SDK 按网络故障重连;在重连窗口调用send入队,恢复后送达且回调一次。 - 原因:文档写明管理员踢下线只断线、令牌仍可用、SDK 应重连。
- 备选:本地 TCP 代理掐线;未采用以减少测试基础设施。
- 实际做法:管理
-
JS 第 14 条跨源
- 实际做法:另起本地 HTTP 端口作为「页面」Origin,对注册接口发带
Origin的 OPTIONS/POST,断言Access-Control-Allow-Origin: *;再用 SDK 从「页面」视角连服务器另一端口的/mqtt。未起真实浏览器。 - 原因:Vitest/Node 无完整浏览器;服务端 CORS 与 WS Origin 策略已由身份/连接线保证。
- 备选:Playwright 实浏览器;本期为控制依赖未引入。
- 实际做法:另起本地 HTTP 端口作为「页面」Origin,对注册接口发带
-
任务 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。 - 影响:无。
- Go:
-
真实 MQTT 收包路径与 request 死锁
- 原条款:DEVELOPMENT 第 9 节收发/确认;回调串行。
- 实际做法:
resp在 MQTTOnPublishReceived路径同步解挂起;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
-
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")。未使用独立 artifacthivemq-mqtt-client-websocket(Maven Central 上该坐标未作为独立稳定模块发布)。 - 原因:与 HiveMQ 官方 WebSocket 用法一致,满足子协议
mqtt。 - 备选方案:若日后官方拆出独立 websocket 模块再改坐标。
- 影响:无行为差异。
-
JSON 库选型
- 原条款:未指定 Java JSON 库。
- 实际做法:Java 用 Gson(
disableHtmlEscaping);Python 用标准库json(ensure_ascii=False)。 - 原因:满足「不转义 HTML / 非 ASCII」;不引入过重依赖。
- 备选方案:Jackson。
- 影响:无。
-
假传输单测,未接真实服务器
- 原条款:任务 1–3 单元测试用假传输;任务 4 才做接入清单。
- 实际做法:Python
FakeTransport、JavaFakeTransport覆盖 Clean Start、去重再 ack、本地超限、令牌回调、重交不改send_at_ms/ 消息号;未做对真实服务器的接入清单(任务 4)。 - 原因:本波范围。
- 备选方案:无。
- 影响:真实联调留待 S2 任务 4。
-
Python 发布元数据
- 原条款:
license = { file = "LICENSE" }与专有分类。 - 实际做法:
pyproject.toml已按此写;包内复制仓库根LICENSE。未配置/执行 PyPI 发布。 - 原因:发布在阶段 3。
- 备选方案:无。
- 影响:无。
- 原条款:
-
Java 编译器用 JDK 21,目标字节码 8
- 原条款:字节码目标 Java 8。
- 实际做法:
maven.compiler.release=8,本机用 Temurin 21 编译。 - 原因:环境已有 JDK 21。
- 备选方案:用 JDK 8 工具链。
- 影响:无。
-
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
-
接入清单对真实 nixmsg,跳过仅 JS 跨域
- 原条款:DEVELOPMENT 第 9 节 15 条;任务 4 用 T0.5 启动器起真实服务端。
- 实际做法:Python
tests/harness.py+test_checklist.py、JavaTestHarness+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 测试不便依赖)。 - 影响:无。
-
清单第 4 条「ack 丢失后服务器重推」未在真机选择性复现
- 原条款:模拟 ack 丢失后服务器重推,SDK 自动再确认且不重复回调。
- 实际做法:集成测验证同消息号防重与断线入队重交送达一次;「选择性丢弃 SDK 发出的 ack 帧」在真实 broker 上做不到,去重再 ack 仍由假传输单测覆盖。
- 原因:不改服务器、无中间代理注入丢包。
- 备选方案:toxiproxy 按包过滤(超出本任务、且难按 MQTT 应用帧过滤)。
- 影响:清单 4 真机为部分通过;假传输路径完整。
-
下行
resp与业务帧分流,避免 auto_ack 自死锁- 原条款:自动模式回调后发 ack;回调串行。
- 实际做法:MQTT/
publishes回调里对resp立即完成 pending;msg等进单线程队列再处理(可在队列线程里同步ack/request)。Paho 使用MQTTv5常量与transport=websockets,并等待 SUBACK。 - 原因:若
resp与msg同队列,auto_ack 等待resp会永久卡住。 - 备选方案:ack 只发布不等待(弱化协议确认)。
- 影响:与 DEVELOPMENT 行为一致,修复真机联调阻塞。
-
HiveMQ 鉴权失败与顶号原因码解析
- 原条款:CONNACK 鉴权失败停重连;
0x8E顶号停重连。 - 实际做法:
connect().get()抛出的Mqtt5ConnAckException/ 文案含BAD_USER_*时归为bad_credentials(令牌场景 Client 层改为session_invalid);断开原因从Mqtt5DisconnectException读SESSION_TAKEN_OVER。connectSync等到终态再返回,避免与 attemptConnect 竞态报busy/RECONNECTING。 - 原因:HiveMQ 失败路径多为异常而非成功返回的 CONNACK 对象。
- 备选方案:无。
- 影响:无。
- 原条款:CONNACK 鉴权失败停重连;
-
任务 5:README/示例与打包试跑,不发布
- 原条款:包名与许可证;工具试跑确认能打包;README 与最小示例;真正发布在 Z3。
- 实际做法:更新两端 README;Python
examples/minimal.py;Javaasia.asio.nixmsg.examples.MinimalExample;python -m build产出 wheel/sdist;mvn package -DskipTests产出 jar;javapmajor version 52(Java 8)。未上传 PyPI/Maven。 - 原因:本波范围。
- 备选方案:无。
- 影响:无。
测试交付 Q
Q1 / Q4 骨架 2026-09-30
-
压测客户端未做到 1000 连接 / 10 分钟
- 原条款:TASKS Q1「1000 连接的压测客户端」;验证「能保持 1000 个连接」。
- 实际做法:
test/load提供 MQTT 3.1.1 CONNECT/CONNACK 骨架与mqttbench命令,单元测试用进程内假 broker 验证多连接与连接数打印;未对真实 broker 压到 1000,也未跑 10 分钟吞吐。 - 原因:本波只做 Q1 基础设施骨架;完整弱网/压测属 Q3,且当前 main 尚无完整 MQTT broker 业务。
- 备选方案:立刻接 mochi 假 broker 或外部 mosquitto 压到 1000。
- 影响:Q3 需补真实压测与报告数字。
-
未做弱网压测与验收用例
- 原条款:TASKS Q2/Q3;本波总控指示「不要做弱网压测和验收用例」。
- 实际做法:只交付 toxiproxy/netem 辅助、报告生成器与空结果样例;F01–F23 全部为未测。
- 原因:波次范围。
- 备选方案:无。
- 影响:交付标准第 2、4 条待后续波次。
-
为 Docker 健康检查在
cmd/nixmsg增加healthcheck- 原条款:DEVELOPMENT 11.2/11.4;TASKS 第 5 节
cmd/nixmsg属平台 P。 - 实际做法:Q 线最小实现
nixmsg healthcheck(读listen.addr或配置listen,请求本机/healthz),否则 compose 健康检查无法按文档工作。 - 原因:Q4 镜像骨架硬依赖该子命令;改动面小。
- 备选方案:等 P 实现后再写 compose;或健康检查改用外部 curl(与 distroless 无 shell/curl 冲突)。
- 影响:P 线后续可替换或扩展实现;注意勿重复注册同名命令。
- 原条款:DEVELOPMENT 11.2/11.4;TASKS 第 5 节
-
Docker 镜像仅当前架构骨架,不推送
- 原条款:DEVELOPMENT 11.4 多架构
buildx推送;TASKS Q4 验证两个架构。 - 实际做法:完善多阶段 Dockerfile +
deploy/docker-compose.yml(镜像名git.asio.asia/nixevol/nixmsg,健康检查/nixmsg healthcheck);本机只docker build当前架构并冒烟/healthz;不做docker push、不做 arm64。 - 原因:总控本波只要骨架;推送在 Z3。
- 备选方案:本波强制 buildx 双架构(耗时长、非阻塞目标)。
- 影响:多架构与仓库推送留到 Q4 定稿 / 阶段 3。
- 原条款:DEVELOPMENT 11.4 多架构
-
compose 示例端口仍写 7443
- 原条款:测试隔离「不要写死 7443」;DEVELOPMENT 11.4 示例为
7443:7443。 - 实际做法:
deploy/docker-compose.yml与文档示例一致使用 7443;混沌辅助与压测工具通过参数传入上游地址,不写死;本地冒烟可用其它宿主机端口映射。 - 原因:部署示例需与 DEVELOPMENT 对齐;隔离约束针对并行测试而非产品默认端口。
- 备选方案:compose 用变量
${NIXMSG_HOST_PORT:-7443}。 - 影响:多 Agent 同时起官方 compose 会端口冲突,应改映射或错开项目名。
- 原条款:测试隔离「不要写死 7443」;DEVELOPMENT 11.4 示例为
-
toxiproxy 镜像与 netem 旁路镜像选型
- 原条款:用 toxiproxy 官方镜像;Linux 丢包用 netem。
- 实际做法:
ghcr.io/shopify/toxiproxy:2.12.0;netem 说明用nicolaka/netshoot挂脚本(需NET_ADMIN)。 - 原因:官方镜像无 tc;本机 Windows 不能本机 netem。
- 备选方案:自建含 iproute2 的旁路镜像。
- 影响:首次拉取 netshoot 需网络;脚本不进业务镜像。
-
增加根目录
.dockerignore- 原条款:未要求;Dockerfile 原为
COPY web/在pnpm install之后。 - 实际做法:忽略
**/node_modules、web/dist、bin等,避免本机 Windows 的node_modules覆盖 Linux 安装结果导致vue-tsc找不到。 - 原因:本机
task check会生成web/node_modules,直接进构建上下文会破坏前端阶段。 - 备选方案:Dockerfile 在
COPY web/后再跑一次pnpm install。 - 影响:镜像构建依赖 dockerignore;与任务目录无关的本地产物不再进上下文。
- 原条款:未要求;Dockerfile 原为
-
命名卷首次需 chown 给 nonroot
- 原条款:DEVELOPMENT 11.4「挂载的数据目录要可写」,uid 65532。
- 实际做法:冒烟前对命名卷执行
chown -R 65532:65532 /data;compose 未内置 init 容器。 - 原因:空命名卷属主为 root 时,distroless nonroot 无法建库(
unable to open database file)。 - 备选方案:compose 增加一次性 init 服务;或文档要求宿主机目录预授权。
- 影响:按示例首次
up前需处理权限,否则 serve 立即退出。
Q2 第一部分(已合并功能验收)2026-09-30
-
对真实进程探测,未接线则记「未测」而非改业务代码
- 原条款: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、注册与开通路径。
-
F22 只验收 init + 健康检查子集
- 原条款:F22 含备份恢复、升级迁移、证书重载、Docker、指标。
- 实际做法:集成测试覆盖空目录
admin init、serve、/healthz、/readyz,并断言管理员密码不出现在 serve 的 stdout/stderr;其余 F22 子项仍标未测。 - 原因:本波范围是「现在就能测的路径」。
- 备选方案:本波强行跑 Docker/证书(超出第一部分)。
- 影响:对照表 F22 为「通过」但备注写明未覆盖项。
-
验收报告写入方式
- 原条款:用
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
-
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、拨钟类与部分身份/回执场景。
-
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 旁路同网再测。
- 影响:丢包数字不进交付;延迟/断开路径有集成测。
-
Q3 压测达不到 1000 连接 / 10 分钟
- 原条款:PRD 第 8 节 / DEVELOPMENT 第 13 节「1000 连接、每秒 200 条、10 分钟」。
- 实际做法:
test/load短时 32 连接(16 对)真实登录+单聊收发成功;不宣称 1000/10min 通过。 - 原因:本机 Windows 开发机资源与并行 Agent 负载;强行 1000 长时间易误伤其他线。
- 备选方案:专用压测机或 Linux 服务器上再跑满指标。
- 影响:对照表与 DEVIATIONS 明示机器限制,不假装通过。
-
崩溃续传用 accept.ManagedServer 启停
- 原条款:提交成功后杀进程,重启后续传。
- 实际做法:
test/accept.ManagedServer(Kill + 同 data_dir Restart)+ Q2/Q3 用例;不改 harness 公共 API(harness 属总控)。 - 原因:隔离目录约束。
- 备选方案:扩展 harness.Restart(需总控改)。
- 影响:Q 线自带启停辅助。
Q4 定稿 + Q5 文档 2026-09-30
-
多架构 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。
- 原条款:DEVELOPMENT 11.4 / TASKS Q4「两个架构的镜像都能初始化、启动并通过健康检查」;
-
交叉编译三平台,本波至少验证 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。
-
compose 容器/卷名带 q4 前缀
- 原条款:DEVELOPMENT 11.4 示例无固定
container_name;测试隔离要求名字带线前缀。 - 实际做法:
deploy/docker-compose.yml使用q4-nixmsg/q4-nixmsg-data;正式部署可去掉container_name。 - 原因:多 Agent 并行不抢容器名。
- 备选方案:compose 用项目名
-p隔离而不写死 container_name。 - 影响:与文档示例略有差异,行为等价。
- 原条款:DEVELOPMENT 11.4 示例无固定
-
验收未测项保持未测
- 原条款:交付标准要求 F01–F23 有结果;TASKS 本波 Q4/Q5 不做假装通过。
- 实际做法:
ACCEPTANCE.md/docs/OPS.md第 9 节明示 F03、F04、F07、F10、F11、F14、F15、F18、F19 仍为未测;不改对照表状态。 - 原因:本波范围是 Docker 定稿与文档。
- 备选方案:无。
- 影响:阶段 3 / 负责人审阅时须看到未测清单。
-
Q5 文档落点
- 原条款:README、运维手册、SDK 文档汇总。
- 实际做法:重写根
README.md;新增docs/OPS.md;SDK 汇总为 README 链到已有sdk/{go,js,python,java}/README.md(各 SDK 已有最短使用说明,本波不重复扩写)。 - 原因:避免四份说明与 SDK 线漂移。
- 备选方案:在 docs/ 再建 SDK 汇总页。
- 影响:无。
Q accept-rest(补齐短时可测验收)2026-09-30
-
补测 F03/F04/F07/F10/F11/F14/F15/F18;F19 引用既有 SDK 清单
- 原条款:PRD 第 10 节;总控要求跳过 1000×10min、Linux netem 20%、1000 端全表 1s。
- 实际做法:
test/accept/rest_accept_test.go用随机端口与临时目录;grace_seconds/ack_timeout_seconds调到数秒;record_retention_days=0另起进程;F19 对照表改为通过并写明四套 SDK checklist 证据路径,本波不重跑全量。 - 原因:短时可测项应收口;长时/环境限制项不假装通过。
- 备选方案:专用压测机与 Linux 宿主再补长时项。
- 影响:
ACCEPTANCE.md汇总通过 23 / 失败 0 / 未测 0;长时子项仍写在备注。
-
harness MQTT 握手后清除 SetDeadline
- 原条款:
test/harness属总控;Dial 时SetDeadline(now+timeout)。 - 实际做法:WebSocket 升级成功与 TCP dial 成功后
SetDeadline(time.Time{}),避免长会话在 dial timeout 到期后读写全部失败。 - 原因:F10 等短宽限仍需跨数秒保持连接;未清 deadline 时旧 10s dial 会在会话中途使 Recv 失败,表现为
timeout waiting resp。 - 备选方案:每次读写刷新 deadline(更繁琐)。
- 影响:跨线改了 harness;行为仅更正测试客户端,不改产品。
- 原条款:
-
F15 带密建群用独立短生命周期进程
- 原条款:拉进群须当次带对话密码。
- 实际做法:主会话用
group.create无密断言失败;带密成功在干净进程上立刻建群。 - 原因:与第 4 条同一死锁,补测时先用隔离进程覆盖校验路径。
- 备选方案:仅依赖第 4 条修复后在同一长会话上测
group.add。 - 影响:验收覆盖仍成立。
-
群事件
emit改为异步 PublishDown- 原条款:群变更向成员推
group_event(QoS 0)。 - 实际做法:
internal/app/group/app.go的emit在独立 goroutine 里延迟约 20ms 再PublishDown,让上行 worker 先把resp推完。 - 原因:同一连接上
group.create/group.add同步向本连接注入下行时,与 mochi InlineClient 互相等待,resp回不去(TestUplinkDMOfflineGroupRecall在清掉测试客户端 dial deadline 后稳定复现)。 - 备选方案:broker 层对 Inline 发布做无锁队列。
- 影响:
group_event可能略晚于resp到达;业务结果仍以resp为准。
- 原条款:群变更向成员推
fix-issue-1
- 管理员 IP 锁定不再阻断已认证会话
- 原条款:PRD D18 / F02(密码锁只拦密码登录,不拦已有会话令牌);DEVELOPMENT 第 5/8 节(管理员登录锁定、错误令牌按 IP 计入锁定);issue #1。
- 实际做法:去掉
internal/admin/auth.go的auth()鉴权前Check(LockAdminIP);登录入口仍Check/Fail,错误或停用 API 令牌仍经authFail计入锁定。有效 Cookie 与合法 Bearer 在锁定期可继续调管理接口。 - 原因:先前把「防暴力登录」扩成「封整个管理面」,同 NAT 下刷错误 Bearer 即可锁死已登录管理员,与端侧 nst_ 重连语义不一致。
- 备选方案:锁定期对 Cookie 与令牌也拒绝(否决,违背 D18 对齐)。
- 影响:仅管理后台鉴权中间件;端侧登录锁定未改。
fix-issue-6
- 解散群时 scheduled 消息级回执 state 改为 rejected
- 原条款:DEVELOPMENT 6.4 消息级作废写
endpoint_id空、state=rejected;7.6 解散群将scheduled消息改为completed/group_dissolved并写消息级回执。I4 旧实现把回执 state 误写成消息状态completed。 - 实际做法:
internal/app/group/void.go的voidGroupAllTx插入回执时改用rejected(与 I5.2 / identity lifecycle 一致);消息行仍为completed。 - 原因:
completed不在回执枚举(accepted|recalled|expired|dropped|rejected)内,会误导 SDK/后台。 - 备选方案:沿用
completed(违反协议)。 - 影响:仅修正解散路径回执字段;不改 emit / PublishDown。
- 原条款:DEVELOPMENT 6.4 消息级作废写
fix-issue-2
- 自助注册接入 trusted_proxies 客户端 IP
- 原条款:PRD F23 / D18 注册安全码按来源 IP 锁定;DEVELOPMENT 4.5 来自受信代理时用
X-Forwarded-For;I1.4 曾写「经代理部署时接线方必须注入真实 IP」。 - 实际做法:
cmd/nixmsg/serve.go在identity.New注入与管理接口相同的httpx.ClientIP(r, trustedNets);不改锁定阈值与注册开关/安全码语义,不在 identity 内复制解析。 - 原因:L-WIRE 已挂注册 Handler,管理与 WS 已接
trusted_proxies,唯独注册漏接,反向代理后会把安全码锁定计到代理 IP。 - 备选方案:在 listener 层统一改写
RemoteAddr后再交给注册 Handler。 - 影响:经受信代理开放注册时,输错安全码按真实客户端 IP 锁定。
- 原条款:PRD F23 / D18 注册安全码按来源 IP 锁定;DEVELOPMENT 4.5 来自受信代理时用
fix-issue-4
- 接线补齐 Downlink 与停用/删除/重置密码 fatal
- 原条款:DEVELOPMENT 6.8 / 7.6:停用、删除、重置密码先发
fatal再断开;已推送作废投递尽力发revoked。 - 实际做法:
serve给identity.New注入Downlink: brk(作废后publishRevokes);DisableKick/DeleteKick/PasswordResetKick分别接到Session.Disable/Deleted/ResetPassword;KickEndpoint仍只Kick。Identity 在未接 Kick 钩子时仍可用ConnControl异步断开兜底。 - 原因:原先 Downlink 未注入导致 revoked 丢失;管理路径只
Kick/Disconnect不发 fatal。 - 备选方案:仅在 identity 内
PublishDown(fatal)再断开;联调中该路径不如 Session.fatalKick 稳,故生产致命踢线统一走 Session。 - 影响:管理「踢下线」语义不变;SDK 可按 fatal 停止重连;接收方能收到已推送消息的 revoked。
- 原条款:DEVELOPMENT 6.8 / 7.6:停用、删除、重置密码先发
fix-issue-5
- 指标在真实事件点打点,不新造名字
- 原条款:PRD F22 / DEVELOPMENT 4.3 / issue #5;DEVIATIONS P4 已定名但从未接线。
- 实际做法:
nixmsg_connections{transport}在 brokerOnSessionEstablished/OnDisconnect末尾 Inc/Dec;endpoints/deliveries_pending/messages_scheduled与写队列、哈希排队在messageLoops每秒按库/队列真实长度采样;dispatch_to_push/ack直方图在成功推送与确认路径 Observe;写批提交耗时经store.Queue.OnBatchCommit;errors_total仅在上行replyErr时按错误码递增。门禁不变。 - 原因:空指标等于监控未交付;采样避免在每条写路径上改大段分发逻辑,并减小与 #3/#6 的合并面。
- 备选方案:全部改为纯事件加减(pending 等需在每处状态迁移维护计数)。
- 影响:仪表盘按既有名字即可看在线连接与待投递;无对应事件时计数保持 0,不做假数。
死锁未修(issue #3)
issue #3 未关闭,feat/fix-3-downlink-deadlock 未合入 main。下面是核对过的调用链、三次尝试和仍留在 main 上的绕过。不改产品行为。
-
现象
- 清掉测试客户端 dial deadline 后,
cmd/nixmsg/uplink_integration_test.go的TestUplinkDMOfflineGroupRecall在group.create(rid=g1)稳定超时,resp回不去。 - 行号以本次合入后的
main为准。
- 清掉测试客户端 dial deadline 后,
-
调用链
- 每端一条上行队列。
internal/broker/queue.go的loop(约 41 行)同步调用HandleUplink。 cmd/nixmsg/uplink.go的HandleUplink(约 64 行)先dispatch,再在同一调用栈里replyOK→publishResp(约 273 行)用 QoS 1 调PublishDown。group.create/group.add走groups.Create/Add,在返回resp之前就emit(internal/app/group/app.go约 168、223 行)。Broker.PublishDown(internal/broker/broker.go约 202 行)进入server.Publish(约 234 行)。New设了InlineClient: true(约 148 行,DEVELOPMENT 要求保持)。Inline 发布走到InjectPacket→OnPublish。internal/broker/hooks.go的OnPublish(约 114 行)对cl.Net.Inline必须直接放行,否则PublishDown送不到订阅者(见本文更早的 InlineClient 偏差)。- 群操作因此在同一次上行调用栈里,再向本连接
PublishDowngroup_event。InjectPacket(NextPacketID/ 写路径)与读循环随后写 PUBACK 抢同一把 Client 锁,两边互等,resp出不去。 presence.notify(internal/app/presence/app.go约 317 行)仍在业务调用栈里同步PublishDown(约 341 行),不在 20ms 绕过的覆盖范围内。
- 每端一条上行队列。
-
已尝试
- 尝试 1:
emit改成立刻起 goroutine 做PublishDown。仍死锁,因为group_event与同连接上的resp一起抢注入。 - 尝试 2(已在
main,来自479a08e的 Q accept-rest 第 4 条):emit起 goroutine 后time.Sleep(20ms)再下发(internal/app/group/app.go约 672–685 行),让resp先出去。当时task check通过。这是时间差绕过,不是根因修复;presence 以及其他同步PublishDown仍可能卡。 - 尝试 3(负责人叫停,未合入、未验证):工作树
e:\code\NixMsg-wt\fix3,分支feat/fix-3-downlink-deadlock停在479a08e,与合入前的origin/main相同,没有可合的提交。未提交改动在 broker 层:hooks.goOnPublish:客户端 QoS≥1 先在读循环里WritePacket(PUBACK),再把包降成 QoS 0 并Ignore,然后入队,返回nil(不再用CodeSuccessIgnore让 mochi 事后写 PUBACK)。注释写明:若先入队,worker 的PublishDown→InjectPacket→NextPacketID会与随后的WritePacket(PUBACK)争 Client 锁。queue.goloop:HandleUplink前后调用beginUplink/endUplink。broker.go:该端depth>0时,对本端的PublishDown只推进延后队列,handler 返回后由上行 worker 再server.Publish;其他端仍同步下发。InlineClient放行未改。group/app.goemit改回同步PublishDown,去掉 20ms sleep。- 同目录
docs/DEVIATIONS.md有一段未提交的### fix-issue-3草稿。
- 本会话没有跑这套未提交代码的
task check,不把它们合进main。工作树保留,给工程师看。
- 尝试 1:
-
仍在 main 上的做法
- 继续用尝试 2 的 20ms 绕过。群事件可能略晚于
resp;业务结果仍以resp为准。
- 继续用尝试 2 的 20ms 绕过。群事件可能略晚于
-
建议的正确方向
- 在 broker 把对本连接的下行
InjectPacket与上行 worker 解耦:上行读循环先写完 PUBACK,处理HandleUplink期间不要同步向本连接注入;handler 返回后再发resp和group_event。不要靠固定Sleep。InlineClient: true保持,OnPublish对 InlineClient 继续放行。 - 覆盖 presence 等其他同步
PublishDown,而不只包一层emit。
- 在 broker 把对本连接的下行