docs: 初始化仓库,加入需求、开发说明和任务拆分
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
---
|
||||
description: NixMsg 多 Agent 开发的分工、分支提交和测试隔离约束
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# NixMsg 开发协作
|
||||
|
||||
开工前先读 `docs/TASKS.md`,确认自己是哪条线、做哪个任务。产品行为以 `docs/PRD.md` 为准,实现以 `docs/DEVELOPMENT.md` 为准,库用法特别看它的第 5 节和第 15 节。
|
||||
|
||||
## 分工
|
||||
|
||||
- 只改自己线负责的目录(TASKS.md 第 5 节);要动共享文件,按 TASKS.md 第 4.2 节的规则。
|
||||
- 文档没写清或互相矛盾时,停下来问总控,不要自己改产品行为。实现和文档不一致,必须写进 `docs/DEVIATIONS.md` 自己那一节。
|
||||
|
||||
## 分支与提交
|
||||
|
||||
- 在自己的工作树和 `feat/<任务号>-<简述>` 分支上工作,不直接改 `main`;只有总控合并 `main`。
|
||||
- 一功能一验证一提交:补测试 → `task check` 通过 → 提交 → 推送自己的分支。
|
||||
- 提交信息 `type: 中文一句话`,type 取 feat、fix、refactor、style、docs、test、chore、revert。
|
||||
- 不提交构建产物、`web/dist`、数据库文件、证书私钥、密码和令牌、`aidocs/`。
|
||||
- 任务完成:rebase 到 `origin/main`,全部验证通过后推送,在 MemRelay 存标签为 `ready-to-merge` 的检查点。
|
||||
- rebase 后用 `git push --force-with-lease` 更新自己的 `feat/*`、`fix/*` 分支(负责人已同意);任何时候都不许强推 `main`。
|
||||
|
||||
## 测试隔离
|
||||
|
||||
- 测试里的服务监听随机端口(`:0`),数据放临时目录,不写死 7443。
|
||||
- Docker 容器、网络、卷的名字带线名前缀,用完删除。
|
||||
- 不改全局 git 配置、系统环境变量和全局 npm 配置。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 后端:Go 标准库 `net/http`、`database/sql` 手写 SQL、SQLite;不引入 Web 框架、ORM、Redis。
|
||||
- 前端:Vue 3 + TypeScript + Naive UI,只用这一套组件库。
|
||||
- 缺工具用 scoop 安装;已确认的决定和依赖库核对结论在 MemRelay 项目记忆(项目 NixMsg)里。
|
||||
|
||||
```yaml
|
||||
# ✅ 测试用的配置:随机端口,数据放临时目录
|
||||
listen: "127.0.0.1:0"
|
||||
data_dir: "<临时目录>/data"
|
||||
# ❌ 写死 listen: ":7443",多个 Agent 同时跑会端口冲突
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.jar binary
|
||||
*.db binary
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# 构建产物
|
||||
/bin/
|
||||
/dist/
|
||||
/web/dist/
|
||||
node_modules/
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
coverage/
|
||||
*.coverprofile
|
||||
|
||||
# 运行数据和本地配置
|
||||
/data/
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
/config.yaml
|
||||
|
||||
# 证书、密钥和环境变量(测试用证书在测试里生成)
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
!**/testdata/**
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# SDK 构建输出
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
/sdk/python/dist/
|
||||
/sdk/js/dist/
|
||||
/sdk/java/**/build/
|
||||
/sdk/java/.gradle/
|
||||
|
||||
# 测试报告
|
||||
/test/reports/
|
||||
/web/test-results/
|
||||
/web/playwright-report/
|
||||
|
||||
# 编辑器和系统文件
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 本地离线记忆,不提交
|
||||
aidocs/
|
||||
@@ -0,0 +1,14 @@
|
||||
# NixMsg
|
||||
|
||||
自建的消息中转服务。设备、程序、App 作为「端」连到同一台服务器,端之间互发消息或群发。单个 Go 程序,内置 MQTT,SQLite 存储,自带管理后台,提供 Go、JS/TS、Python、Java/Android SDK。
|
||||
|
||||
## 文档
|
||||
|
||||
- [产品需求](docs/PRD.md)
|
||||
- [开发说明](docs/DEVELOPMENT.md)
|
||||
- [开发任务拆分与多 Agent 协作](docs/TASKS.md)
|
||||
- [与文档的偏差](docs/DEVIATIONS.md)
|
||||
|
||||
## 状态
|
||||
|
||||
需求和设计已完成,正在开发。构建、运行和部署说明在开发完成后补充。
|
||||
+1198
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
# 实现与文档的偏差
|
||||
|
||||
开发中凡是实现和 [PRD.md](./PRD.md)、[DEVELOPMENT.md](./DEVELOPMENT.md) 不一致的地方,都记在这里,交给负责人审核。
|
||||
|
||||
每条写清:日期、原条款(文档和小节)、实际做法、原因、影响。各线只写自己那一节,避免多条线同时改同一段。
|
||||
|
||||
## 总控 L
|
||||
|
||||
暂无。
|
||||
|
||||
## 平台 P
|
||||
|
||||
暂无。
|
||||
|
||||
## 连接 N
|
||||
|
||||
暂无。
|
||||
|
||||
## 消息 M
|
||||
|
||||
暂无。
|
||||
|
||||
## 身份 I
|
||||
|
||||
暂无。
|
||||
|
||||
## 后台接口 A
|
||||
|
||||
暂无。
|
||||
|
||||
## 后台网页 W
|
||||
|
||||
暂无。
|
||||
|
||||
## SDK 一 S1
|
||||
|
||||
暂无。
|
||||
|
||||
## SDK 二 S2
|
||||
|
||||
暂无。
|
||||
|
||||
## 测试交付 Q
|
||||
|
||||
暂无。
|
||||
+581
@@ -0,0 +1,581 @@
|
||||
# NixMsg 产品需求
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-30 |
|
||||
| 状态 | 待复核后交给开发 |
|
||||
| 读者 | 产品负责人、开发组、后续审核 |
|
||||
| 配套 | [DEVELOPMENT.md](./DEVELOPMENT.md) 是实现规范。两者冲突时,以本文已确认的产品行为为准,并在 `docs/DEVIATIONS.md` 记录 |
|
||||
| 修订 | 见第 11 节 |
|
||||
|
||||
## 1. 这是什么
|
||||
|
||||
NixMsg 是一套自建的消息中转服务。设备、程序、App 都作为「端」连到同一台服务器,端只用一个固定端口。端之间用端编号互发小消息,或在群里发同一条消息。
|
||||
|
||||
服务器负责:身份(开通、自助注册、登录)、在线状态、弱网重传、可选的离线暂存、定时和延迟发送、撤回、送达结果。服务器不保存聊天记录,也不提供业务聊天界面。业务界面由接入方自己做。我们提供管理后台和各语言 SDK。
|
||||
|
||||
一条消息的正文最大 256 KiB。更大的文件本期不传,以后用「先上传、再发链接」的方式做。
|
||||
|
||||
## 2. 要解决的问题
|
||||
|
||||
- 端与端之间隔着 NAT,不能互相直连,需要一台中转。
|
||||
- 网络会抖、会断。已经交给服务器的消息要尽量送到,对方短暂掉线时不要立刻丢掉。
|
||||
- 对方长时间不在线时,发送方自己决定留不留、留多久。
|
||||
- 发送方可以反悔,也可以指定过一会儿再发。
|
||||
- 多个端要能收同一条消息。
|
||||
- 有的端不希望陌生人给它发消息。
|
||||
- 接入方要在自己的 App 里让用户注册、登录、改密码,但不能让陌生人随便注册。
|
||||
- 接入方语言不同,需要少写连接代码;没有现成 SDK 的设备要能按协议直接接。
|
||||
|
||||
## 3. 名词
|
||||
|
||||
| 名词 | 含义 |
|
||||
|---|---|
|
||||
| 端 | 一个通讯身份。有唯一编号、登录密码,既能发也能收。程序、设备、App 登录后都是端。端由管理员开通,或在管理员开启注册后自己注册 |
|
||||
| 群 | 若干端的集合。发到群里的一条消息,内容只有一份,每个成员各有自己的送达结果 |
|
||||
| 登录密码 | 端连接服务器时使用。防止别人冒充这个编号 |
|
||||
| 会话令牌 | 用密码登录成功后服务器发给端的一串随机字符。之后断线重连都用它,不用密码,也和 IP 无关。每次用密码登录都会换新的,旧的立即作废 |
|
||||
| 对话密码 | 端自己可选的一道门。别人要单聊它,或把它拉进群,得先知道这道密码。不设置则谁都能给它发 |
|
||||
| 对话授权 | 服务器记住的「甲可以向乙单聊」。输对一次对话密码,或乙主动给甲发过单聊,都会形成授权。乙改对话密码后授权失效 |
|
||||
| 注册安全码 | 管理员设置的一串字符。开启自助注册后,注册时必须带上它。更换后只影响之后的注册 |
|
||||
| 提交 | 发送方把消息交给服务器,服务器写入存储并返回成功 |
|
||||
| 发送时刻 | 服务器真正开始投递的时间。可以是立即、延迟若干秒,或指定的未来时间 |
|
||||
| 投递 | 服务器把消息推给接收端 |
|
||||
| 收下 | 接收端处理完并向服务器确认。这时才算送到 |
|
||||
| 确认超时 | 消息推给在线端后,等它确认的最长时间,默认 5 分钟 |
|
||||
| 离线保留 | 发送时刻对方不在线时,把消息留在服务器,等它上线再推。按条选择 |
|
||||
| 保留时间 | 离线保留的最长期限,从发送时刻起算。超时还没收下就作废 |
|
||||
| 抖动宽限 | 对方刚断线的一小段时间(默认 60 秒)。这段里即使本条没有选择保留,也先等它重连 |
|
||||
| 撤回 | 发送方取消一条对方还没收下、且还没作废的消息 |
|
||||
| 回执 | 服务器告诉发送方某个接收端的最终结果:已收下、已过期、已丢弃、已拒绝。撤回的结果从撤回请求的返回里得到,不再发回执 |
|
||||
| 记录 | 不含正文的投递痕迹:谁发给谁、何时提交、何时发送、结果如何。给管理后台查询 |
|
||||
| 圈子 | 这一台服务器上的全部端。谁都能看到别人在不在线 |
|
||||
|
||||
## 4. 谁在用
|
||||
|
||||
| 角色 | 做什么 |
|
||||
|---|---|
|
||||
| 程序或设备 | 主要使用者。自动收发,收到就处理 |
|
||||
| 人 | 通过接入方自己的 App 或网页看消息、点发送;管理员开启注册时,也可以在接入方 App 里自己注册。本期不提供这套界面 |
|
||||
| 管理员 | 使用自带的网页后台:开通端、开关注册和设置注册安全码、改密码、看在线、管群、查送没送到 |
|
||||
|
||||
典型场景:
|
||||
|
||||
1. 后台给设备下发一条指令。设备当时不在线,指令保留 24 小时,上线后送到。设备回复时,若后台设了对话密码且曾经给设备发过单聊,设备不用再输密码。
|
||||
2. 人在网页里给一个端发消息,发出后 10 秒内可以撤回。
|
||||
3. 约定明天 8 点发到一个群。到点时按当时的群成员投递;选了保留的,不在线的成员上线后仍能收到同一条内容。
|
||||
4. 发送前先看对方在不在线,或列出当前在线的端。
|
||||
5. 接入方在自己的 App 里内置注册安全码,用户在 App 里注册、登录、改密码。管理员更换安全码后,旧版 App 不能再注册新用户,已注册的用户照常使用。
|
||||
|
||||
## 5. 本期做、本期不做
|
||||
|
||||
### 5.1 本期做
|
||||
|
||||
| 编号 | 能力 | 优先级 |
|
||||
|---|---|---|
|
||||
| F01 | 开通和管理端 | P0 |
|
||||
| F02 | 登录,同一编号只能一处在线 | P0 |
|
||||
| F03 | 查看在线和端目录 | P0 |
|
||||
| F04 | 上下线通知 | P0 |
|
||||
| F05 | 单聊 | P0 |
|
||||
| F06 | 群发 | P0 |
|
||||
| F07 | 消息内容与 256 KiB 上限 | P0 |
|
||||
| F08 | 至少送达一次、确认、去重、顺序 | P0 |
|
||||
| F09 | 离线保留和保留时间 | P0 |
|
||||
| F10 | 断线抖动宽限 | P0 |
|
||||
| F11 | 定时发送 | P0 |
|
||||
| F12 | 发送延迟(反悔窗口) | P0 |
|
||||
| F13 | 撤回 | P0 |
|
||||
| F14 | 回执 | P0 |
|
||||
| F15 | 对话密码和授权 | P0 |
|
||||
| F16 | 群管理 | P0 |
|
||||
| F17 | 网页管理后台和管理 API 令牌 | P0 |
|
||||
| F18 | 正文删除和投递记录 | P0 |
|
||||
| F19 | Go、Java/Android、JS/TS、Python SDK | P0 |
|
||||
| F20 | 无 SDK 时按协议用 MQTT 接入 | P0 |
|
||||
| F21 | 端只用一个固定端口,后台可用单独端口 | P0 |
|
||||
| F22 | 单文件或 Docker 部署、备份、升级、监控指标 | P0 |
|
||||
| F23 | 端自助注册(管理员开关、注册安全码) | P0 |
|
||||
|
||||
### 5.2 本期不做
|
||||
|
||||
- 大文件、分块、断点续传。后续另行做「上传文件后发送链接」。
|
||||
- 服务器保存历史供翻看。历史由各端自己存。
|
||||
- 同一编号同时在多台设备登录。要多台就开通多个端。
|
||||
- 多机集群。
|
||||
- 注册审核、邮箱或手机号验证。注册只靠注册安全码把关。
|
||||
- 忘记密码自助找回。忘记登录密码由管理员重置。
|
||||
- 端自己注销。需要时由管理员在后台删除。注意:接入方的 iOS App 如果提供注册,苹果审核要求 App 内能删除账号,届时需要补这项能力。
|
||||
- 已读(人有没有看)。只有「端有没有收下」。
|
||||
- 编辑已发消息。可以撤回后重发。
|
||||
- 端到端加密。传输可以用 TLS,服务器在投递完成前能看到正文。
|
||||
- 手机系统推送(APNs、FCM 等)。注意:手机 App 退到后台后,连接通常会被系统挂起,没有系统推送就收不到新消息提醒;人用的聊天场景要考虑这一点。
|
||||
- 管理员在后台代发或代撤回。
|
||||
- 多个管理员角色。本期一个管理员账号。
|
||||
- 嵌入式 C 语言 SDK。小设备按 F20 直接接 MQTT。
|
||||
|
||||
## 6. 功能需求
|
||||
|
||||
优先级 P0 为本期必须完成。每条都包含规则和验收。
|
||||
|
||||
### F01 开通和管理端
|
||||
|
||||
端由管理员开通,或在管理员开启注册后由端自己注册(F23)。两种方式开出来的端没有区别。
|
||||
|
||||
- 编号 1–64 字符,只能是小写字母、数字、`_`、`.`、`-`,全局唯一。留空则服务器生成,形如 `e_` 加 8 位小写字母数字。不允许大写,是为了避免 `Alice` 和 `alice` 这类看起来相同的编号互相冒充。
|
||||
- 名称可选,最多 64 个 Unicode 字符。备注可选,最多 200 字符。
|
||||
- 登录密码 8–128 字符,不能以 `nst_` 开头(这个前缀留给会话令牌)。留空则生成随机密码,只在创建成功时显示一次。
|
||||
- 对话密码可选,4–64 字符。空表示不设防。
|
||||
- 可设置默认发送延迟,单位秒,默认 0。
|
||||
- 支持批量开通:上传 CSV 文件(UTF-8),列为编号、名称、登录密码、对话密码、默认延迟、备注,一次最多 1000 行。先整体检查,有任何一行不合格就一行都不开通,并列出出错的行和原因。密码留空则生成,生成的密码只在结果里出现这一次,可以下载保存。
|
||||
- 可改名称、备注、默认延迟、启停、删除、重置登录密码、设置或清除对话密码、踢下线、解除登录锁定。重置登录密码会让它的会话令牌作废。踢下线只断开当前连接,令牌不变,SDK 会自动重连;要让它连不上,请停用。
|
||||
- 停用:立刻断开,不能再登录;发给它的新消息提交失败;尚未送到它的消息作废,原因 `endpoint_disabled`;它自己发出、还没推送出去的消息也作废,原因 `sender_disabled`。重新启用后可以正常登录,已作废的消息不恢复。
|
||||
- 删除:在停用的效果之外(原因记为 `endpoint_deleted`、`sender_deleted`),退出所有群;它当群主的群转给最早加入的成员,群里没有别人则解散;清掉与它相关的授权、它的待送回执和它发出的消息记录。编号删除后可以重新开通或注册,新端不继承任何旧数据。
|
||||
|
||||
验收:
|
||||
|
||||
- 一次批量开通 1000 个端成功,生成的密码不写入日志。CSV 里有一行编号重复时,一个端都不开通,并指出是哪一行。
|
||||
- 停用后该端立刻离线,发给它的新消息被拒绝,发给它的定时未到点的消息作废;它停用前提交、还没到点的定时消息也不再发出。
|
||||
- 删除群主后,群主变为剩余成员里最早加入的那个。
|
||||
- 删除一个端后用同一编号重新开通,新端收不到旧端的回执。
|
||||
|
||||
### F02 登录、会话令牌与单连接
|
||||
|
||||
- 首次登录用端编号和登录密码。登录成功后服务器发给端一个会话令牌,之后断线重连都用这个令牌,不再用密码。令牌和 IP 无关:设备换网络、换 IP,或者服务器重启后,都直接用令牌重连。
|
||||
- 同一编号同时只有一个有效会话。每次用密码登录都换一个新令牌,旧令牌立即作废(D28):
|
||||
- 旧设备当时在线:当场被踢下线,原因是「已在别处登录」,停止自动重连并通知应用。
|
||||
- 旧设备当时不在线:之后用旧令牌重连会被拒绝,SDK 通知应用「会话已失效」并停止重连,需要重新用密码登录。
|
||||
- 用令牌重连不算新登录,令牌不变。
|
||||
- 应用应该保存令牌,而不是密码。令牌 30 天没用过自动失效(可配置,0 表示不失效),之后要重新用密码登录(D29)。应用退出登录时调用 SDK 的退出,服务器作废令牌。
|
||||
- 端可以自己改登录密码,必须提供旧密码。当前连接保持,并拿到新令牌。
|
||||
- 管理员重置登录密码、停用、删除:令牌作废,当前连接被踢下线,停止自动重连。管理员「踢下线」只断开连接,令牌不变,SDK 会自动重连。
|
||||
- 密码输错会临时锁定。锁定只针对用密码登录,用令牌重连不受影响,所以已登录的设备换 IP、断线重连都不会被锁(D18):
|
||||
- 同一编号在同一来源 IP 上 5 分钟内错 10 次,锁定这个组合 5 分钟。设备换了 IP,不会被别的 IP 上的失败牵连。
|
||||
- 同一编号不论来源 IP,1 小时内错 50 次,暂停这个编号的密码登录 1 小时,防止有人不断换 IP 猜密码。管理员可以在后台解除锁定。
|
||||
- 登录失败要分清两类:密码错误、会话已失效、编号不存在、已停用、已锁定,SDK 停止重连并通知应用;服务器暂时不可用(例如数据库故障、连接数已满),SDK 继续重连。
|
||||
|
||||
验收:
|
||||
|
||||
- A 设备在线时,B 设备用密码登录同一编号:A 被踢下线且不再重连,B 可用。
|
||||
- A 设备离线期间,B 设备用密码登录;A 恢复网络后用旧令牌重连被拒,SDK 报「会话已失效」且不再重连。
|
||||
- 设备换了 IP(例如 Wi-Fi 切到 4G)后,用令牌自动重连成功,不需要密码。
|
||||
- 错误密码达到上限后,同一 IP 用正确密码在锁定期内也被拒绝,锁定期过后可以登录;换一个 IP 用正确密码不受影响。
|
||||
- 从多个 IP 轮流猜同一编号的密码,达到总数后这个编号暂停密码登录;已登录的设备断线后用令牌照常重连;管理员解除锁定后可以用密码登录。
|
||||
- 服务器数据库不可用时,SDK 不会进入「密码错误」状态,恢复后自动连上。
|
||||
|
||||
### F03 在线状态与目录
|
||||
|
||||
任何已登录的端都可以:
|
||||
|
||||
- 查询若干编号是否在线。
|
||||
- 分页列出全部端。每项包含编号、名称、是否在线、最近上线时间、最近离线时间、是否设置了对话密码。不包含密码本身,也不包含「我是否已获授权」。
|
||||
|
||||
在线以服务器上的真实连接为准。心跳超时未收到数据即判离线。默认心跳 30 秒,超时按协议的 1.5 倍,约 45 秒。
|
||||
|
||||
验收:
|
||||
|
||||
- 正常断开后 1 秒内,别人查到的状态为离线。
|
||||
- 拔掉网线后,在心跳超时窗口内变为离线。
|
||||
- 1000 个端同时在线时,拉全表在 1 秒内返回。
|
||||
|
||||
### F04 上下线通知
|
||||
|
||||
端可以订阅某些编号的上下线,也可以订阅全部。变化时收到通知。通知尽力送达,不落库、不补发。连接断开后订阅清空,SDK 在重连后按应用上次的选择重新订阅;应用需要准确状态时,重连后再查一次在线状态。
|
||||
|
||||
验收:订阅 A 后,A 上下线各收到一次通知;没订阅的 B 上下线不收到。
|
||||
|
||||
### F05 单聊
|
||||
|
||||
- 端可以给另一个端发消息,也可以给自己发(用于定时提醒)。发给自己不需要对话密码。
|
||||
- 每条消息由发送方提供消息号,SDK 自动生成。1–64 字符,字母、数字、`_`、`.`、`-`,区分大小写。同一发送方不要重复使用消息号。
|
||||
- 服务器把消息落入存储后才返回提交成功。返回前宕机,客户端按未提交处理并重试。
|
||||
- 用同一消息号、同一请求内容重试,返回原来的结果,不产生第二条;即使对方在两次之间停用或改了对话密码,也以第一次的结果为准。同一消息号但请求内容不同(目标、正文、选项有任何不同),返回冲突。服务器至少在 24 小时内(防重窗口,可配置)识别重试;这条消息的记录还在时也一直识别。
|
||||
- 对方不存在、已停用、已删除:提交失败并给出原因。
|
||||
- 对方设了对话密码且发送方没有有效授权:提交失败,错误为需要对话密码。本条请求里带上正确密码则授权并提交成功。
|
||||
- 每个端默认每秒最多 50 个请求(可短时突发到 100,确认类请求不算),可配置。用一个端集中下发大量消息的接入方,可以调高或改用群发。
|
||||
- 每个端同时处于「等待发送」或「投递中」的消息最多 10000 条(群消息算一条),超出时提交失败;每个端排队等它收下的投递最多 10000 条,超出时新来的投递直接拒绝,原因 `queue_full`,发送方收到回执。两个上限都可配置,设为 0 表示不设上限(D25),用来防止一个端把服务器磁盘塞满。
|
||||
|
||||
验收:
|
||||
|
||||
- 提交成功后立刻杀掉服务器进程,重启后这条消息仍会投递。
|
||||
- 同一消息号连续提交两次,接收方只处理一次。同一消息号换了正文再提交,返回冲突。
|
||||
- 未授权时不带密码被拒绝;带对密码后成功,之后不带密码也能再发。
|
||||
- 一个端未完成的消息达到上限后,新提交失败;接收端排队达到上限后,新投递被拒绝,发送方收到回执。
|
||||
|
||||
### F06 群发
|
||||
|
||||
- 提交时发送方必须是群成员。到发送时刻发送者已不在群里:消息作废,不再投递。
|
||||
- 内容只存一份。每个接收成员一条投递结果。
|
||||
- 接收者是发送时刻的成员,不含发送者本人。已停用的成员收不到,结果记为已拒绝。
|
||||
- 发送时刻之后才入群的端收不到。发送时刻之前已退群的端收不到。
|
||||
- 群消息不看成员的对话密码。
|
||||
|
||||
验收:
|
||||
|
||||
- 群内 A、B、C,A 发送,B 和 C 收到同一消息号、同一内容,A 自己不收到。
|
||||
- A 发送时选了离线保留,B 当时离线,B 在保留时间内上线后收到的内容与 C 相同。
|
||||
- B 在发送时刻之后入群,收不到这条。
|
||||
|
||||
### F07 内容与大小
|
||||
|
||||
- 正文是 UTF-8 文本,或二进制(线上用 Base64)。可带内容类型。
|
||||
- 自定义键值附在消息上,序列化后不超过 4 KiB。
|
||||
- 正文解码后最大 256 KiB(262144 字节)。这个上限可以调小,本期不能调大。超过则发送端直接失败,服务器不接收。
|
||||
- 接收端可在登录握手时声明自己一次最多能收多少字节(按服务器推来的整条数据计算,不小于 1024)。某条投递超过该值:这条投递拒绝,原因 `too_large`,不重试,并回执发送方。
|
||||
- 不做分块,不做断点续传。
|
||||
|
||||
验收:
|
||||
|
||||
- 256 KiB 文本可以送达。多 1 字节在 SDK 本地失败;绕过 SDK 直接提交过大的正文,服务器拒绝且不产生投递。
|
||||
- 声明只能收 1024 字节的端,收到一条 2048 字节消息的投递结果是拒绝,发送方收到对应回执,这个端的连接不受影响。
|
||||
|
||||
### F08 投递、确认、重复和顺序
|
||||
|
||||
- 到了发送时刻且对方已完成登录握手:立刻推送。
|
||||
- 接收方处理完成后确认。SDK 默认在收到回调正常返回后自动确认;也可以改成应用自己调用确认。
|
||||
- 处理回调抛错则不确认。选了离线保留的消息,在保留时间内服务器会在重连后或确认超时后再推;不保留的消息推送后确认超时仍未确认,按已丢弃结束(D19)。应用必须能接受同一条到达多次。
|
||||
- 保证的是至少送到一次。弱网上可能重复。SDK 在进程运行期间按「发送方编号 + 消息号」去重后再交给应用,默认记住最近 10000 条;已经确认过的消息再次到达时,SDK 直接再确认一次,不交给应用。进程重启后,重启前没确认的消息可能再到一次。
|
||||
- 同一接收端按发送时刻排序;发送时刻相同则按提交先后。SDK 对同一连接上的回调串行执行。重推的消息可能排在后面的消息之后。
|
||||
- MQTT 传输层的确认只表示服务器收到了请求。对方收下以应用层确认为准。
|
||||
|
||||
验收:
|
||||
|
||||
- 弱网(延迟、丢包、随机断开)下,选择了离线保留且在保留期内的消息最终全部送达;SDK 进程不重启时,交给应用的回调不重复。
|
||||
- 服务器在已返回提交成功后强制重启,未收下的消息会继续投递。
|
||||
|
||||
### F09 离线保留
|
||||
|
||||
- 每条消息选择是否保留,默认不保留。
|
||||
- 保留时指定时长,默认 24 小时,最长 30 天(可配置)。从发送时刻起算,定时等待的时间不算进去。
|
||||
- 超时仍未收下:结果为已过期,正文删除。
|
||||
- 已经推给在线端、正在等确认的消息,这一轮等待(最长一个确认超时)里不会因为保留时间到了而作废;这一轮结束仍未确认且已过保留期限,按已过期结束,否则再推。
|
||||
|
||||
验收:
|
||||
|
||||
- 保留 1 小时,对方 30 分钟后上线,能收到。
|
||||
- 对方 2 小时后才上线,收不到,发送方得到已过期。
|
||||
- 指定明天 8 点发送、保留 1 小时:在明天 8 点之前一直处于等待发送;8 点才开始算 1 小时。
|
||||
|
||||
### F10 抖动宽限
|
||||
|
||||
- 不保留,且发送时刻对方不在线:若对方在宽限内刚断线(默认 60 秒),先等到宽限结束;期间重连则照常推送,宽限结束仍不在则丢弃。
|
||||
- 从未上线或断线已超过宽限:不保留的消息立即丢弃。
|
||||
- 正在推送时连接断开:无论是否保留,至少再等一个宽限。保留消息的截止时间若早于「现在 + 宽限」,延长到「现在 + 宽限」。
|
||||
- 服务器重启时,所有未完成的投递按对方刚断线处理:至少再等一个宽限,端在宽限内重连就能收到(D23)。
|
||||
|
||||
验收:
|
||||
|
||||
- 不保留。对方断网 30 秒内恢复,消息送到。
|
||||
- 不保留。对方断网超过 2 分钟,消息丢弃,发送方收到已丢弃。
|
||||
- 不保留。服务器重启后端在 30 秒内重连,重启前没收下的消息仍能送到。
|
||||
|
||||
### F11 定时发送
|
||||
|
||||
- 发送方指定未来的发送时刻,以服务器时钟为准。SDK 用握手返回的服务器时间校正本机偏差。
|
||||
- 提交成功后发送方可以下线,到点由服务器投递,然后再走 F09、F10。
|
||||
- 最远可定到 365 天后(可配置)。指定时间已过则立即发送。
|
||||
- 服务器停机期间到点的定时消息,启动后立即发送。
|
||||
- 定时与「延迟秒数」不要同时使用。同时使用则提交失败。指定了发送时刻时,不再叠加端的默认延迟。
|
||||
|
||||
验收:提交一条 2 分钟后发送的消息,发送方立刻断开;2 分钟后接收方在线则收到,不在线则按是否保留处理。
|
||||
|
||||
### F12 发送延迟
|
||||
|
||||
- 端可以固定默认延迟。每条消息也可以单独指定延迟秒数,指定 0 表示这条立即发送。都没指定则立即发送。
|
||||
- 延迟由服务器时钟计算:发送时刻 = 服务器当前时间 + 延迟。
|
||||
- 在发送时刻之前消息还在服务器上,撤回一定成功。这就是反悔窗口。
|
||||
|
||||
验收:默认延迟 10 秒。发送后 3 秒撤回,接收方永远收不到。超过 10 秒且对方在线,消息已经进入投递。
|
||||
|
||||
### F13 撤回
|
||||
|
||||
- 只有发送方能撤回。管理员不能代撤。
|
||||
- 对象是已提交、接收方尚未收下、且尚未结束(过期、丢弃、拒绝)的消息。
|
||||
- 还没到发送时刻:一定成功,之后不再发送。
|
||||
- 已在离线保留中、尚未推送:一定成功。
|
||||
- 已经推给在线端但对方还没确认:撤回和确认谁先被服务器记下来谁生效。撤回先到,则对方之后的确认会得知已撤回,SDK 发出撤回事件;应用如果已经处理了这条,由应用自己决定怎么撤销(例如隐藏)。确认先到则撤回失败(或群里该成员失败)。
|
||||
- 已经收下、已过期、已丢弃、已拒绝:不能撤回。
|
||||
- 群消息:还没收下的成员全部撤回;已经收下的成员保持已收下。至少撤回一个且没有成员已收下,结果为「全部撤回」;有撤回也有已收下,为「部分撤回」;一个都没撤回,为「失败」。已经过期、丢弃、拒绝的成员不影响结果。
|
||||
- 撤回的结果在撤回请求的返回里给出,不再为撤回单独发回执(D20)。
|
||||
- 撤回不收回已经形成的回复授权(F15 第 2 种授权,D22)。
|
||||
|
||||
验收:
|
||||
|
||||
- 延迟窗口内撤回,接收方无回调。
|
||||
- 对方离线且在保留中,撤回后上线也收不到。
|
||||
- 群里一人已确认、一人未推送:结果为部分撤回,未推送的成员收不到,已确认的成员仍保留本地副本。
|
||||
|
||||
### F14 回执
|
||||
|
||||
- 每条消息默认要回执,可以按条关闭。
|
||||
- 每个接收端一条最终回执:已收下、已过期、已丢弃、已拒绝。
|
||||
- 发送方当时不在线:回执保留,默认最多 7 天,上线后补送。回执可以重复到达,SDK 按回执号去重。
|
||||
- 关闭回执的消息不占这份队列。管理后台仍能按 F18 查记录。
|
||||
|
||||
验收:接收方确认后发送方收到已收下。发送方离线期间对方确认,发送方重连后补收到回执。
|
||||
|
||||
### F15 对话密码
|
||||
|
||||
- 不设置:任何端都能向它单聊。
|
||||
- 设置后,向它单聊必须有有效授权。授权只有两种:
|
||||
1. 发送时或单独解锁时输对当前对话密码。
|
||||
2. 对方曾经成功提交过发给我的单聊。群消息不算。
|
||||
- 授权只看提交那一刻。提交成功之后对方才改密码,这条已提交的消息照常投递。改密码只让之后的新发送重新验证。
|
||||
- 对方改密码后,旧授权全部失效,包括「它先给我发过消息」带来的回复权。
|
||||
- 把它拉进群时,这一次请求里必须带上它当前的对话密码,已有单聊授权也不能省略。管理员在后台拉人例外,不需要密码。
|
||||
- 进群之后,群消息直接送达,不再逐条要密码。
|
||||
- 对话密码连续猜错会临时锁定这一对端。默认与登录锁定相同:5 分钟 10 次。另外按被猜的端计总数:1 小时内被猜错 50 次,暂停验证它的对话密码 1 小时,已有授权照常可用;它改对话密码时计数清零(D24)。这是为了防止有人注册很多账号轮流猜,4 位的对话密码经不起这样猜。
|
||||
- 不希望被大量回复的发送方(例如给很多端发通知的后台端),可以改用群发:群消息不产生回复授权。
|
||||
|
||||
验收:
|
||||
|
||||
- B 设了密码。A 不带密码发送失败;带对一次后,第二条不再带密码也成功。
|
||||
- B 改密码后,A 的下一条失败,直到重新输对新密码。
|
||||
- B 先给 A 发单聊,A 立刻可以回复且不用密码。B 改密码后,A 再回复失败。
|
||||
- A 已能给 B 单聊,把 B 拉进群时仍要带对话密码;密码错误则 B 不进群。
|
||||
- B 改密码时,A 此前已提交、尚在延迟中的消息到点仍会送达。
|
||||
- 多个账号轮流猜 B 的对话密码,达到总数上限后,正确密码也暂时无法解锁;已有授权的端照常发给 B。
|
||||
|
||||
### F16 群
|
||||
|
||||
- 已登录的端可以建群,建群者是群主。管理员也可以建群并指定群主,群主同时是成员。
|
||||
- 编号规则同端编号(小写字母、数字、`_`、`.`、`-`),留空则生成,形如 `g_` 加 8 位。名称最多 64 字。
|
||||
- 群主可以改名、加人、踢人、转让群主、解散。成员可以自己退出。群主不能直接退出,要先转让;只剩群主一人时可以解散。
|
||||
- 加人时对设了对话密码的端适用 F15。已停用的端不能加入。一次请求里可以带多个成员,校验失败的成员不加入,其余加入,结果里列出失败原因。
|
||||
- 踢出或退出:尚未推送的投递作废,原因 `left_group`;已经推送、尚未确认的,向该端发作废通知。
|
||||
- 解散:全体未完成投递作废;发往该群、还没到发送时刻的消息也立即作废,原因 `group_dissolved`。群编号之后可以被重新使用,新群收不到旧群的任何消息。
|
||||
- 群主被删除:群主转给最早加入的其他成员;没有其他成员则解散。
|
||||
- 成员数默认上限 1000,可配置。
|
||||
|
||||
验收:
|
||||
|
||||
- 非群主加人失败。群主把设密端拉入时密码错误,该端不在成员里,群本身创建成功。
|
||||
- 成员退出后收不到之后的消息,也收不到退出前已提交但还没推送的消息。
|
||||
- 解散群后立刻用同一编号建新群,旧群里还没到点的定时消息不会发给新群。
|
||||
|
||||
### F17 管理后台
|
||||
|
||||
网页后台,界面中文。功能:
|
||||
|
||||
- 登录、修改管理员自己的密码、退出。
|
||||
- 概览:端数量(其中自助注册的数量)、在线数、群数量、待投递数、版本。
|
||||
- 端:查询(可按来源筛选:后台开通、自助注册)、开通、批量开通、编辑、启停、删除、重置登录密码、设置或清除对话密码、踢下线、解除登录锁定。列表显示是否在线、最近上下线时间、来源;支持多选后批量停用、删除,方便清理异常注册。
|
||||
- 注册:开启或关闭自助注册;查看、手填或生成注册安全码。
|
||||
- 群:查询、创建、改名、成员增减、转让、解散。
|
||||
- 投递记录:按发送方、接收方、群、状态、时间筛选。只显示记录,不显示正文。点开可看每个接收端的结果、原因、推送次数、时间。
|
||||
- API 令牌:管理员可以创建多个令牌(填名称),令牌只在创建时显示一次;可以停用、启用、删除,列表显示最近使用时间。接入方自己的后台带令牌调用同一套管理接口,用程序完成开通、停用、重置密码等操作。令牌不能改管理员密码,也不能管理令牌(D27)。
|
||||
- 每个改变状态的操作都记日志:谁做的(管理员或哪个令牌)、做了什么、对象是谁。
|
||||
- 运行参数本页只读,改参数通过配置文件。管理员密码、注册开关、注册安全码和 API 令牌在后台修改。
|
||||
|
||||
验收:
|
||||
|
||||
- 用后台完成开通、改密、开启注册并设置安全码、建群、查看一条未完成投递。正文在页面、接口和浏览器网络响应里都不出现。
|
||||
- 创建一个 API 令牌,用它通过管理接口开通一个端并重置密码;用它改管理员密码被拒绝;停用令牌后立即不能再用。
|
||||
|
||||
### F18 正文删除与记录
|
||||
|
||||
- 一条消息的全部投递都进入最终状态,或在发送前被撤回、作废:立即删除正文。
|
||||
- 记录默认保留 7 天,可配置。设为 0 则完成后连记录一起删除,后台只能看到尚未完成的消息。
|
||||
- 防重标记另存「发送方 + 消息号 + 请求指纹」,不含正文。默认保留 24 小时,消息记录还在时一直保留。这是为了弱网重试不产生第二条,不是历史记录。
|
||||
- 日志里不写正文、登录密码、对话密码、注册安全码。
|
||||
- 备份文件里包含备份当时还没送完的正文,要按敏感数据保管。
|
||||
|
||||
验收:接收方确认后,数据库和后台都读不到正文。保留天数设为 0 时,完成后后台列表不再出现这条。24 小时内重试同一消息号仍不重复投递。
|
||||
|
||||
### F19 SDK
|
||||
|
||||
第一批:Go、Java/Android、JavaScript/TypeScript(浏览器和 Node.js)、Python。
|
||||
|
||||
四种 SDK 行为一致:
|
||||
|
||||
- 注册(管理员开启注册时可用,需要注册安全码,不需要先登录)。
|
||||
- 登录与会话:首次用密码登录,把拿到的会话令牌交给应用保存;之后重连用令牌;令牌失效时通知应用重新登录;退出登录时作废令牌。
|
||||
- 连接、自动重连、心跳。被踢下线、密码错误、会话已失效、编号停用或删除时停止重连;服务器暂时不可用时继续重连。
|
||||
- 发送(单聊、群、延迟、定时、是否保留、保留时长、是否要回执、附带对话密码)。
|
||||
- 撤回、查询本条状态、解锁对话密码。
|
||||
- 收消息、收回执、收撤回或作废、去重、自动或手动确认。
|
||||
- 在线查询、目录、上下线订阅。
|
||||
- 建群、改名、加人、踢人、退出、转让、解散、我的群列表。
|
||||
- 修改自己的名称、默认延迟、登录密码(需要旧密码)、对话密码。
|
||||
- 发送方暂时连不上服务器时,发送进入内存队列,重连后按原消息号再交;已经发出但没等到结果的也一样。进程退出则队列丢失。除发送以外的请求在离线时直接失败。
|
||||
- 本机时间与服务器偏差由 SDK 校正后再计算定时发送。
|
||||
|
||||
验收:每种语言都通过 DEVELOPMENT.md 里的同一份接入清单。清单覆盖注册、登录、收发、去重、重连、离线队列、撤回、定时、群、对话密码、改密、被踢、超限。
|
||||
|
||||
### F20 无 SDK 接入
|
||||
|
||||
不提供 C SDK。设备用 MQTT 5 按 DEVELOPMENT.md 的帧格式接入,至少能:登录、收消息、确认、发送、声明自己能接收的最大字节数。能发 HTTP 请求的设备也可以调用注册接口;不能的由管理员开通。设备可以每次都用密码登录(每次都算新登录、会换令牌,只有这一台设备时没有影响),也可以保存会话令牌用来重连。
|
||||
|
||||
验收:用一个不依赖四套 SDK 的 MQTT 客户端走通登录、发送、接收、确认。
|
||||
|
||||
### F21 端口
|
||||
|
||||
- 端只用一个固定端口连服务器(默认 7443)。这个端口同时提供:MQTT over WebSocket(路径 `/mqtt`,子协议 `mqtt`)、MQTT over TCP(给跑不动 WebSocket 的设备)、注册接口。TLS 和明文在同一端口上自动识别。
|
||||
- 管理后台默认也在这个端口。可以配置成单独端口,例如只监听内网或本机地址,不对公网暴露;配置后,端接入端口不再提供后台。
|
||||
- 内置 MQTT 不单独占端口。本期不需要 Redis 或其他中间件。
|
||||
- 配置了证书则默认只接受 TLS。没有证书时允许明文,并在启动日志里明确警告。是否额外允许明文可配置。
|
||||
- 证书文件更新后自动生效,不用重启。程序本身不申请证书;生产环境由服务器上的 1Panel 申请和自动续签,并推送到本地目录给程序读取(见 DEVELOPMENT.md 第 11.3 节)。
|
||||
|
||||
验收:
|
||||
|
||||
- 默认配置只开一个端口:浏览器能打开后台,SDK 能用 WebSocket 注册和收发,一个裸 MQTT TCP 客户端能登录。
|
||||
- 配置单独的后台端口后:后台只能从后台端口打开,端接入端口上访问后台返回 404,端的注册和收发不受影响。
|
||||
|
||||
### F22 部署与运维
|
||||
|
||||
- 一个 Go 程序,一个可执行文件,内置 MQTT 和网页。不另外部署数据库、消息队列或缓存。
|
||||
- 同时提供 Docker 镜像(amd64、arm64)和 docker-compose 示例,装有 1Panel 的服务器可以直接用它的容器编排部署。
|
||||
- 数据在一个目录里的 SQLite 文件。
|
||||
- 首次使用先运行初始化命令生成管理员密码(只在终端显示一次,不写日志),再启动服务。
|
||||
- 提供备份命令。升级时自动迁移数据库;迁移前自动把库复制一份。
|
||||
- 提供健康检查接口,以及 Prometheus 格式的监控指标(在线数、待投递数、投递耗时等,不含正文和编号明细)。指标只在后台端口上开放;后台和端共用端口时,要带配置的令牌才能访问。
|
||||
|
||||
验收:
|
||||
|
||||
- 空目录运行初始化命令后启动,可登录后台。二进制和 Docker 镜像(两个架构)都能这样启动。
|
||||
- 备份文件能在另一目录启动并看到原来的端。旧版本数据经过一次升级后端和未完成消息都在。
|
||||
- 替换证书文件后,不重启服务,新连接在 1 小时内用上新证书。
|
||||
- Prometheus 能抓到指标;共用端口时不带令牌抓不到。
|
||||
|
||||
### F23 端自助注册
|
||||
|
||||
管理员可以允许端自己注册,接入方就能在自己的 App 里做注册、登录、改密码。
|
||||
|
||||
- 默认关闭。管理员在后台开启,并设置注册安全码(8–64 字符,可以手填或让系统生成)。关闭时任何注册都失败。
|
||||
- 注册必须带当前的注册安全码,错误则失败。
|
||||
- 管理员随时可以更换安全码或关闭注册。已注册的端不受影响,照常登录收发;之后的注册必须用新安全码,旧码无法注册。
|
||||
- 注册时填写:编号(可留空,由服务器生成)、登录密码(可留空,由服务器生成并只返回一次)、名称(可选)、对话密码(可选)。规则同 F01。编号已被占用则失败。
|
||||
- 注册成功即可登录,不需要管理员审核。后台把这类端标记为「自助注册」,管理员可以像其他端一样停用、删除、重置密码。
|
||||
- 注册在端接入端口上完成,不需要先登录。同一来源 IP 连续输错安全码会临时锁定:默认 5 分钟 10 次,锁定 5 分钟。
|
||||
- 注册之后的改名称、改默认延迟、改登录密码(需要旧密码)、改对话密码,都由端登录后通过 SDK 完成(F19)。
|
||||
- 开放注册后,注册者和其他端一样能看到目录(全部端的编号、名称和在线状态),也能给没设对话密码的端发消息。圈子规则不变(D26)。需要防陌生人的端应设置对话密码。
|
||||
- 安全码内置在 App 里时,拿到安装包的人就能取出来。它挡的是没有 App 的人,不能证明注册者身份;泄露后换码即可,已注册的端不受影响。
|
||||
- 服务对公网开放注册时应配置证书,否则安全码和密码会以明文经过网络。
|
||||
|
||||
验收:
|
||||
|
||||
- 注册关闭时,带正确安全码也注册失败。
|
||||
- 开启后,错误安全码失败,正确安全码成功,注册出的端能立即登录。
|
||||
- 管理员更换安全码后,旧码注册失败、新码成功;更换前已注册的端照常登录收发。
|
||||
- 同一 IP 连续输错安全码达到上限后,锁定期内带正确安全码也被拒绝。
|
||||
- 编号已存在时注册失败,原有端不受影响。
|
||||
|
||||
## 7. 消息怎么走
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 等待发送: 提交成功
|
||||
等待发送 --> 已撤回: 发送时刻前撤回
|
||||
等待发送 --> 已拒绝: 到点前或到点时发现对方已停用或删除、群已解散、发送者已退群或被停用
|
||||
等待发送 --> 投递中: 到发送时刻
|
||||
投递中 --> 已收下: 接收端确认
|
||||
投递中 --> 已撤回: 确认前撤回成功
|
||||
投递中 --> 已过期: 保留期限到,或确认超时时已过保留期限
|
||||
投递中 --> 已丢弃: 不保留且宽限结束仍离线,或推送后确认超时
|
||||
投递中 --> 已拒绝: 退群、停用、删除、超过对方接收上限等
|
||||
已收下 --> [*]
|
||||
已撤回 --> [*]
|
||||
已过期 --> [*]
|
||||
已丢弃 --> [*]
|
||||
已拒绝 --> [*]
|
||||
```
|
||||
|
||||
群消息是同一张图的多份投递。全部投递都结束后删除正文。
|
||||
|
||||
| 结果 | 何时 | 发送方能否撤回 |
|
||||
|---|---|---|
|
||||
| 等待发送 | 没到发送时刻 | 能,一定成功 |
|
||||
| 投递中,还没推送 | 离线保留、宽限中,或在排队等推送 | 能,一定成功 |
|
||||
| 投递中,已推送 | 在线但还没确认 | 能发起,和确认抢先 |
|
||||
| 已收下 | 端已确认 | 不能 |
|
||||
| 已过期 / 已丢弃 / 已拒绝 | 已经结束 | 不能 |
|
||||
|
||||
## 8. 非功能需求
|
||||
|
||||
| 项 | 要求 |
|
||||
|---|---|
|
||||
| 规模 | 单机 1000 个端同时在线,端总数按 1 万设计。内部连接上限按 2000 留余量 |
|
||||
| 容量 | 记录量约等于每天消息数乘以保留天数。按每天 100 万条、保留 7 天设计,后台带筛选的查询在 1 秒内返回 |
|
||||
| 吞吐 | 验收环境能持续每秒 200 条单聊提交;一条 1000 人群消息能在 5 秒内完成推送排队 |
|
||||
| 延迟 | 双方在线、消息小于 1 KiB 时,从提交成功到对方回调,机房内 P95 小于 300 毫秒 |
|
||||
| 可靠 | 已返回提交成功的消息,进程崩溃后仍在。断电最多丢失最后一次尚未落盘的写入;默认使用最严格的 SQLite 同步 |
|
||||
| 弱网 | 20% 丢包、2 秒延迟、随机断开下,保留期内消息最终全部送达,SDK 进程不重启时应用层无重复 |
|
||||
| 安全 | 密码单向哈希,会话令牌和管理 API 令牌只存哈希;登录、对话密码、注册安全码、管理员登录和令牌都防暴力尝试;后台 Cookie 不可被脚本读取;日志无正文、无密码、无注册安全码、无令牌;端只能订阅自己的下行 |
|
||||
| 时间 | 服务器时钟为准。定时消息在时钟被人为大改时按新时钟触发,这一点写入运维说明 |
|
||||
| 兼容 | MQTT 5 为主。MQTT 3.1.1 仅保证能连接和收发 QoS 1,帧格式仍是同一套 JSON;3.1.1 设备被顶号时看不到原因 |
|
||||
|
||||
## 9. 写进本文的默认
|
||||
|
||||
下面是写文档时定下的默认规则。「已确认」是负责人已经认可的;「待确认」的在交给开发前还可以改,改的话同步改本文和 DEVELOPMENT.md 对应小节。D14 起是 0.2 新增,D26 起是 0.3 新增,D28 起是 0.4 新增。
|
||||
|
||||
| 编号 | 默认 | 状态 |
|
||||
|---|---|---|
|
||||
| D1 | 改对话密码不影响已经提交成功的消息 | 已确认 2026-09-30 |
|
||||
| D2 | 拉人进群必须在当次请求里带对话密码;已有单聊授权不能代替。管理员后台例外 | 已确认 2026-09-30 |
|
||||
| D3 | 回执默认开启,发送方离线时回执最多留 7 天 | 已确认 2026-09-30 |
|
||||
| D4 | 不保留的消息,对方断线未超过 60 秒时先等重连 | 已确认 2026-09-30 |
|
||||
| D5 | 投递记录默认留 7 天且不含正文;防重标记另留 24 小时 | 已确认 2026-09-30 |
|
||||
| D6 | 离线保留默认 24 小时,最长 30 天;端默认延迟 0;定时最远 365 天 | 已确认 2026-09-30 |
|
||||
| D7 | 允许给自己发消息 | 已确认 2026-09-30 |
|
||||
| D8 | 接收端可声明最大接收字节,超限的投递直接拒绝 | 已确认 2026-09-30 |
|
||||
| D9 | 停用或删除端时,尚未送到的消息作废 | 已确认 2026-09-30 |
|
||||
| D10 | 群主被删除时,群主转给最早加入的成员 | 已确认 2026-09-30 |
|
||||
| D11 | 正文用 UTF-8 或 Base64,自定义字段最多 4 KiB | 已确认 2026-09-30 |
|
||||
| D12 | 一个管理员账号;后台不能代发、代撤回 | 已确认 2026-09-30 |
|
||||
| D13 | 被踢下线的 SDK 停止自动重连 | 已确认 2026-09-30 |
|
||||
| D14 | 自助注册默认关闭。注册安全码 8–64 字符,明文存库、可在后台查看(要分发给接入方;负责人确认不考虑数据库泄露的风险),不写日志 | 已确认 2026-09-30 |
|
||||
| D15 | 注册时编号可以自选也可以留空生成;登录密码留空则服务器生成并只返回一次;注册不需要审核 | 已确认 2026-09-30 |
|
||||
| D16 | 不做忘记密码自助找回和端自己注销,都由管理员处理 | 已确认 2026-09-30 |
|
||||
| D17 | 端编号和群编号只允许小写字母、数字、`_`、`.`、`-`;消息号区分大小写 | 已确认 2026-09-30 |
|
||||
| D18 | 登录密码失败按「编号 + 来源 IP」锁定(5 分钟 10 次),另按编号计总数暂停密码登录(1 小时 50 次);会话令牌重连不受锁定影响;注册安全码按来源 IP 锁定 | 按 2026-09-30 反馈修改,待确认 |
|
||||
| D19 | 不保留的消息推给在线端后,确认超时(5 分钟)仍未确认就按已丢弃结束,不再重推;保留的消息在保留期内继续重推 | 已确认 2026-09-30 |
|
||||
| D20 | 撤回不单独发回执,结果看撤回请求的返回 | 已确认 2026-09-30 |
|
||||
| D21 | 停用或删除端时,它发出、还没推送的消息一并作废 | 已确认 2026-09-30 |
|
||||
| D22 | 撤回不收回已经形成的回复授权 | 已确认 2026-09-30 |
|
||||
| D23 | 服务器重启时,所有未完成投递至少再等一个宽限,包括保留期在停机期间已过的 | 已确认 2026-09-30 |
|
||||
| D24 | 对话密码另按被猜的端限总数:1 小时错 50 次后暂停新的验证 1 小时,已有授权不受影响 | 已确认 2026-09-30 |
|
||||
| D25 | 每个端未完成的发出消息最多 10000 条,排队待收的投递最多 10000 条;都可配置,设为 0 表示不设上限 | 已确认 2026-09-30 |
|
||||
| D26 | 开放自助注册后,目录仍对所有端可见,谁都能列出全部端 | 已确认 2026-09-30 |
|
||||
| D27 | 管理 API 令牌不过期,停用或删除后立即失效;权限等同管理员,但不能改管理员密码、不能管理令牌;令牌只在创建时显示一次 | 待确认 |
|
||||
| D28 | 每次用密码登录都换新的会话令牌,旧令牌立即作废,旧设备自动退出 | 已确认 2026-09-30(负责人提出) |
|
||||
| D29 | 用令牌重连不换令牌;令牌 30 天没用自动失效(可配置,0 表示不失效);登录密码不能以 `nst_` 开头;端可以主动退出登录、作废令牌 | 待确认 |
|
||||
|
||||
## 10. 验收总表
|
||||
|
||||
开发完成时逐条给出结果:通过、失败或未测。未测要写原因。
|
||||
|
||||
| 编号 | 一句话 |
|
||||
|---|---|
|
||||
| F01 | 批量开通整批校验、停用、删除群主转让、删除后同编号重开不串数据 |
|
||||
| F02 | 新设备登录后旧设备自动退出(在线被踢、离线的令牌失效)、换 IP 用令牌重连、两种密码锁定、重置密码后被踢、服务器故障不误报密码错误 |
|
||||
| F03 | 断开后状态及时变离线,全表可列出 |
|
||||
| F04 | 只通知订阅了的端 |
|
||||
| F05 | 崩溃不丢已提交消息,消息号去重和冲突,密码门生效,配额生效 |
|
||||
| F06 | 群成员收到同一份,入群前不补,发送者不收到自己的 |
|
||||
| F07 | 256 KiB 通过,超出拒绝,接收上限生效 |
|
||||
| F08 | 弱网最终送达且应用层不重复,重启后续传 |
|
||||
| F09 | 保留时间从发送时刻起算,超时过期 |
|
||||
| F10 | 短断线送到,长断线丢弃,服务器重启后宽限内重连送到 |
|
||||
| F11 | 发送方离线后到点仍发送 |
|
||||
| F12 | 延迟窗口内撤回对方收不到 |
|
||||
| F13 | 未推送必撤成功;群部分确认得到部分撤回 |
|
||||
| F14 | 回执能补送给当时离线的发送方 |
|
||||
| F15 | 输一次记住、改密失效、回复免密、进群仍要密码、防多账号轮流猜 |
|
||||
| F16 | 群主权限、退出后不再收到、解散后同编号新群不收旧消息 |
|
||||
| F17 | 后台管端、管注册、管群、查记录,响应里没有正文;API 令牌可用且不能越权 |
|
||||
| F18 | 送达后正文消失;记录天数 0 时连记录消失;防重仍在 |
|
||||
| F19 | 四种 SDK 通过同一清单 |
|
||||
| F20 | 裸 MQTT 能登录、收、确认、发 |
|
||||
| F21 | 默认一个端口提供后台、WebSocket、TCP、注册;后台可分到单独端口 |
|
||||
| F22 | 初始化后单文件或 Docker 启动、备份恢复、升级迁移、证书自动重载、指标可抓取 |
|
||||
| F23 | 注册开关、安全码校验、换码不影响已注册、输错锁定 |
|
||||
|
||||
## 11. 修订记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|---|---|---|
|
||||
| 0.1 | 2026-09-30 | 初稿 |
|
||||
| 0.2 | 2026-09-30 | 端口改为「端只用一个固定端口,后台可用单独端口」(F21)。新增端自助注册(F23),从「本期不做」中移除。按审查补充和修正:编号只允许小写(F01、F16);批量开通整批校验(F01);停用或删除端时作废它发出的消息,删除后编号可复用(F01);登录锁定按编号加 IP,并区分服务器故障(F02);消息号按请求内容判冲突,防重标记与记录并存(F05、F18);请求频率上限写入需求(F05);接收上限按整条数据计算(F07);不保留消息的确认超时(F08、F09);服务器重启后的宽限和补发(F10、F11);撤回结果的判定和不发回执(F13、F14);解散群时作废定时消息(F16);后台批量停用、删除和注册设置(F17);首次启动先初始化(F22);单位统一为 KiB;重试先认防重再校验(F05);每个端未完成消息和排队投递的上限(F05);对话密码另按被猜的端限总数(F15);后台端列表显示在线状态(F17);注册安全码和明文传输的提醒(F23);手机 App 在后台收不到提醒的说明(5.2);新增默认 D14–D25 |
|
||||
| 0.3 | 2026-09-30 | 负责人确认第 9 节 D1–D26(D27 待确认):开放注册后目录仍对所有端可见(D26),配额可设为 0 表示不设上限(D25)。新增管理 API 令牌和管理操作日志(F17)。F22 增加 Docker 镜像和 Prometheus 监控指标。F21 写明证书文件自动重载、生产环境用 1Panel 申请和续签。技术栈写入 DEVELOPMENT.md 第 2 节:Go + 标准库 net/http、SQLite + 手写 SQL、不用 Redis、Vue 3 + TypeScript + Naive UI 打包嵌入程序 |
|
||||
| 0.4 | 2026-09-30 | 登录改为会话令牌(F02):首次用密码登录后发令牌,重连用令牌、和 IP 无关;每次用密码登录都换新令牌,旧设备自动退出(D28);令牌闲置 30 天失效,可以主动退出登录(D29)。密码锁定改为按编号加 IP、按编号总数两种,只拦密码登录,不拦令牌重连(D18);后台可以解除锁定(F01、F17);登录密码不能以 `nst_` 开头。新增 docs/TASKS.md 开发任务拆分 |
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
# NixMsg 开发任务拆分与多 Agent 协作
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-30 |
|
||||
| 对应 | [PRD.md](./PRD.md) 0.4、[DEVELOPMENT.md](./DEVELOPMENT.md)(对应 PRD 0.4) |
|
||||
| 仓库 | https://git.asio.asia/nixevol/NixMsg.git,主分支 `main` |
|
||||
| 读者 | 总控 Agent、各开发线 Agent、负责人 |
|
||||
|
||||
## 1. 目标和交付标准
|
||||
|
||||
由多个 Agent 并行开发和测试,交付 NixMsg 服务端、管理后台、四种 SDK、Docker 镜像和文档。
|
||||
|
||||
下面全部满足才算交付:
|
||||
|
||||
1. `main` 上 `task check`(构建、代码检查、单元测试)和全部集成测试通过。
|
||||
2. PRD 第 10 节 F01–F23 每条都有验收结果:通过;或未测,写明原因并经负责人认可。
|
||||
3. 四种 SDK 都通过 DEVELOPMENT 第 9 节的接入清单。
|
||||
4. DEVELOPMENT 第 13 节的弱网、崩溃、压测、端到端测试做完并有报告。
|
||||
5. 二进制(`linux/amd64`、`linux/arm64`、`windows/amd64`)和 Docker 镜像(amd64、arm64)能按文档启动。
|
||||
6. 和文档不一致的实现都写进 `docs/DEVIATIONS.md`,并经负责人确认。
|
||||
7. 最终代码按第 7 节审核过,打上 `v0.1.0` 标签并推送。
|
||||
|
||||
## 2. 资料
|
||||
|
||||
- `docs/PRD.md`:产品行为,以它为准。
|
||||
- `docs/DEVELOPMENT.md`:实现规范。第 5 节的库用法和第 15 节「不要这样做」必须看。
|
||||
- `docs/TASKS.md`:本文件。
|
||||
- MemRelay 项目记忆(项目 NixMsg,ID `25464650-ed5d-440e-b362-190f2852d451`):已确认的决定、技术栈选型、依赖库行为核对结论,以及各线的进度检查点。随时可以查。
|
||||
- 文档没写清或互相矛盾时,停下来问总控;总控拿不准的问负责人。不要自己改 PRD 的产品行为。
|
||||
|
||||
## 3. 环境
|
||||
|
||||
- 本机是 Windows,已有 Git、Go、Node.js LTS、pnpm、Python、JDK 21、Docker Desktop 和 scoop。缺的工具用 scoop 装,例如 `scoop install task golangci-lint`;包名不确定时先 `scoop search`。
|
||||
- 丢包模拟(netem)、多架构镜像等 Linux 相关的测试,在 Docker 的 Linux 容器里跑。toxiproxy 用它的官方 Docker 镜像。
|
||||
- 多个 Agent 在同一台机器上同时跑测试,必须互不干扰:
|
||||
- 测试里的服务一律监听随机端口(`:0`),数据放临时目录,不要写死 7443。
|
||||
- Docker 容器、网络、卷的名字带上自己的线名前缀,用完删除。
|
||||
- 不改全局配置:全局 git 配置、系统环境变量、全局 npm 配置都不要动。
|
||||
- 推送 git.asio.asia 用本机 Windows 凭据管理器里已存好的账号,不要把账号密码写进仓库、脚本或命令行。
|
||||
|
||||
## 4. 协作规则
|
||||
|
||||
### 4.1 分支、工作树和提交
|
||||
|
||||
- `main` 只由总控合并,任何时候都要能构建、测试全绿。
|
||||
- 每条线在自己的 git 工作树(worktree)里、自己的分支上工作,不共用目录。用 Cursor 代理窗口的工作树功能创建,或者自己建:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git worktree add ../NixMsg-wt/<任务号> -b feat/<任务号>-<简述> origin/main
|
||||
```
|
||||
|
||||
- 分支名:`feat/<任务号>-<简述>`,例如 `feat/n2-websocket-mochi`;修别人发现的问题用 `fix/<任务号>-<简述>`。
|
||||
- 一功能一验证一提交:做完一个能验证的小功能,就补测试、跑 `task check`、提交,并推送到自己的远端分支。
|
||||
- 提交信息:`type: 中文一句话描述`,type 取 feat、fix、refactor、style、docs、test、chore、revert。
|
||||
- 不提交:构建产物、`web/dist`、数据库文件、证书和私钥、任何密码或令牌、`aidocs/`。
|
||||
- 只改自己线负责的目录(第 5 节)。必须动别人的目录时,先告诉总控,改动尽量小。
|
||||
|
||||
### 4.2 共享文件
|
||||
|
||||
| 文件 | 规则 |
|
||||
|---|---|
|
||||
| `go.mod`、`go.sum` | 各线可以加依赖;冲突时以 main 为准重新 `go mod tidy` |
|
||||
| `internal/protocol` | 总控在阶段 0 定好。之后要改先找总控,由总控改完合进 main,各线再 rebase |
|
||||
| `internal/store/migrations` | `0001` 由总控在阶段 0 写好,包含全部表。之后要改表结构就加新文件,编号在 rebase 时取当时最大号加一 |
|
||||
| `cmd/nixmsg/wire.go`(组装各模块) | 各线只加自己的注册代码;冲突时两边都保留 |
|
||||
| `Taskfile.yml` | 总控维护;各线自己的任务写在 `taskfiles/<线名>.yml`,由主文件引入 |
|
||||
| `docs/DEVIATIONS.md` | 按线分节,各线只写自己那节 |
|
||||
| `docs/PRD.md`、`docs/DEVELOPMENT.md`、`docs/TASKS.md` | 只有总控改,而且要先得到负责人同意 |
|
||||
|
||||
### 4.3 合并进 main
|
||||
|
||||
1. 开发线:`git fetch` → `git rebase origin/main` → `task check` 和本任务的验证全过 → 推送分支 → 在 MemRelay 存一个检查点,标签 `ready-to-merge`,写清分支名、任务号、验证结果。
|
||||
2. 总控:按第 7 节清单审阅差异 → 在主工作区 `git merge --ff-only <分支>` → 跑 `task check` 和集成测试 → 推送 main → 通知各线 rebase。
|
||||
3. 不能快进(main 已经前进)时打回给开发线重新 rebase。冲突由开发线理解双方改动后解决。
|
||||
4. 功能分支 rebase 后,用 `git push --force-with-lease` 更新自己的远端分支(负责人已同意,见第 9 节)。只能用在自己的 `feat/*`、`fix/*` 分支上,不许用于别人的分支;任何人都不许强推 `main`。
|
||||
5. 如果仓库开了分支保护或合并请求审核,改成推分支、建合并请求,由总控合并。
|
||||
|
||||
### 4.4 进度记录
|
||||
|
||||
- 各 Agent 按协作规则使用 MemRelay:开工前读项目上下文和最新检查点;完成一个任务、遇到阻塞、结束一次会话时存检查点,写明线名、任务号、分支、做了什么、验证结果、下一步。
|
||||
- 不在仓库里另建进度文件,免得多条线同时改同一个文件。
|
||||
|
||||
## 5. 分工
|
||||
|
||||
| 线 | 负责目录 | 主要内容 |
|
||||
|---|---|---|
|
||||
| 总控 L | 根目录配置、`cmd/nixmsg/wire.go`、`internal/protocol`、迁移 `0001`、`test/harness`、`docs/` | 阶段 0、审阅合并、集成验证、最终验收 |
|
||||
| 平台 P | `cmd/nixmsg`(`wire.go` 除外)、`internal/config`、`internal/store`(`0001` 除外)、`internal/auth` | 配置和命令、写入合并、密码哈希池、令牌和锁定计数、指标注册 |
|
||||
| 连接 N | `internal/listener`、`internal/broker` | 端口识别、TLS、WebSocket、mochi、登录与会话令牌、连接表、握手 |
|
||||
| 消息 M | `internal/app/message` | 提交、分发、推送、确认、撤回、回执、清理、启动恢复 |
|
||||
| 身份 I | `internal/app/identity`、`internal/app/group`、`internal/app/presence` | 注册、自己的资料、对话密码、在线与目录、群、停用和删除 |
|
||||
| 后台接口 A | `internal/admin`、`internal/httpx` | 管理接口、API 令牌、操作日志、指标接口的访问规则 |
|
||||
| 后台网页 W | `web/` | Naive UI 后台和端到端测试 |
|
||||
| SDK 一 S1 | `sdk/go`、`sdk/js` | Go、JS/TS SDK |
|
||||
| SDK 二 S2 | `sdk/python`、`sdk/java` | Python、Java/Android SDK |
|
||||
| 测试交付 Q | `test/`(`harness` 除外)、`deploy/`、`README.md` | 测试基础设施、验收用例、弱网和压测、Docker、发布、运维文档 |
|
||||
|
||||
## 6. 阶段和任务
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
T0[阶段0 总控 骨架与契约] --> P[平台 P]
|
||||
T0 --> N[连接 N]
|
||||
T0 --> M[消息 M]
|
||||
T0 --> I[身份 I]
|
||||
T0 --> A[后台接口 A]
|
||||
T0 --> W[后台网页 W]
|
||||
T0 --> Q[测试交付 Q]
|
||||
N --> S[SDK S1 S2 接入清单]
|
||||
M --> S
|
||||
I --> S
|
||||
A --> W2[网页联调 W4]
|
||||
P & N & M & I & A & S & W2 --> V[阶段2 验收 弱网 压测]
|
||||
V --> Z[阶段3 总控 审核与交付]
|
||||
```
|
||||
|
||||
- **阶段 0**(只有总控,串行):T0.1–T0.5。全部合进 main 并推送后才开阶段 1。
|
||||
- **阶段 1**(并行):P、N、M、I、A、W、Q 同时开工,共 7 个 Agent。S1、S2 可以一起开工,先做不依赖服务端的部分(连接状态机、发送队列、去重、本地检查和单元测试),也可以等 N、M 合进 main 再开,看机器负载。
|
||||
- **阶段 2**(联调和验收):N、M、I、A 的核心任务合进 main 后,S1、S2 跑接入清单,W 接真实接口做端到端测试,Q 跑验收、弱网、崩溃和压测。发现的问题开 `fix/` 分支,交回原负责线修。
|
||||
- **阶段 3**(只有总控):全量回归、审核、偏差确认、打标签交付。
|
||||
|
||||
下面每个任务都要做到:代码、测试、文档依据里的规则都满足;验证项全部通过;`task check` 通过。
|
||||
|
||||
### 6.1 阶段 0:总控 L
|
||||
|
||||
**T0.1 仓库骨架** · 依赖:无
|
||||
|
||||
- 做:按 DEVELOPMENT 第 3 节建目录;`go.mod`(模块 `git.asio.asia/nixevol/NixMsg`);`Taskfile.yml`,目标至少有 `build`、`test`、`lint`、`check`(构建加检查加单元测试)、`web:dev`、`web:build`、`itest`(集成测试)、`docker`,并引入 `taskfiles/*.yml`;golangci-lint 配置;`web/` 用 Vite、Vue 3、TypeScript、Naive UI、Vue Router、Pinia 初始化,加 `web/embed.go`;`cmd/nixmsg` 先能 `version`,`serve` 先只打开库、跑迁移、在 `listen` 上提供 `/healthz`,端口为 0 时写 `listen.addr`;`deploy/config.example.yaml`;`.cursor/worktrees.json`,让新工作树自动执行 `go mod download` 和 `pnpm install`。
|
||||
- 验证:Windows 上 `task check`、`task build` 通过,产物是嵌入了前端的单个可执行文件;在 Docker 的 Linux 容器里 `task check` 也通过。
|
||||
|
||||
**T0.2 协议包 `internal/protocol`** · 依赖:T0.1
|
||||
|
||||
- 做:DEVELOPMENT 第 6 节全部帧的 Go 类型,包括第 6.9 节注册的请求和响应、`session_token`、`self.logout`;第 6.10 节错误码常量;编码规则(不转义 HTML 和非 ASCII);字段校验(编号规则、正文和自定义字段大小、`send_at_ms` 和 `delay_ms` 互斥);第 7.3 节的请求指纹;整帧字节数计算。
|
||||
- 验证:每种帧的编码、解码、校验都有表驱动单元测试;指纹对 `meta` 的键顺序不敏感。
|
||||
|
||||
**T0.3 数据库结构和存储接口 `internal/store`** · 依赖:T0.1
|
||||
|
||||
- 做:第 7.7 节的迁移执行器(有未应用版本时先 `VACUUM INTO` 备份);`0001_init.sql` 包含第 7.7 节全部表和索引;按第 7.7 节的 DSN 打开写连接和读连接池;写入队列的接口(提交一个写操作、拿到结果)和一个先不合并的简单实现,P2 再换成合并提交。
|
||||
- 验证:空目录启动能建表;重复启动不会重复迁移;迁移前的备份文件存在;有单元测试。
|
||||
|
||||
**T0.4 模块接口和管理接口契约** · 依赖:T0.2、T0.3
|
||||
|
||||
- 做:`internal/auth` 和 `internal/app/*` 各服务的 Go 接口,以及测试用的假实现;broker 和 app 之间的接口(连接事件、上行帧分发、下行发布);`cmd/nixmsg/wire.go` 组装骨架;`docs/api/admin-api.md`,写清第 8 节每个管理接口的请求、响应、分页和错误格式,W 线据此先用假数据开发。
|
||||
- 验证:`task check` 通过;契约文档覆盖第 8 节全部路由。
|
||||
|
||||
**T0.5 集成测试启动器 `test/harness`** · 依赖:T0.1
|
||||
|
||||
- 做:编译一次服务端;每个测试用随机端口、临时目录启动一个进程(配置里 `listen` 的端口写 0,服务端启动后把实际地址写进 `<data_dir>/listen.addr`,见 DEVELOPMENT 第 4.1 节,启动器读它),自动执行 `admin init` 拿到管理员密码;提供管理接口客户端和 MQTT 测试客户端(WebSocket 和 TCP 都支持,能收发第 6 节的帧);测试结束时清理进程和目录。
|
||||
- 验证:示例集成测试能启动服务、访问 `/healthz`,结束后进程和目录都清干净;两个测试并行跑互不干扰。
|
||||
|
||||
### 6.2 平台 P
|
||||
|
||||
**P1 配置和命令** · 依赖:T0.1、T0.3
|
||||
|
||||
- 做:第 11.1 节配置的加载和校验(`check-config` 拒绝 `max_body_bytes` 大于 262144);`admin init`、`admin set-password`、`backup`、`healthcheck`;`log/slog` JSON 日志;第 7.8 节的优雅停机。
|
||||
- 验证:各命令有测试;`admin init` 的密码只在终端打印一次、不进日志,已初始化的库拒绝再次执行。
|
||||
|
||||
**P2 写入合并** · 依赖:T0.3
|
||||
|
||||
- 做:第 7.2 节的合并事务(最多 256 个操作或凑满 2 毫秒)、每个操作一个 SAVEPOINT、读连接池;写库失败时请求返回 `busy`,`/readyz` 返回失败。
|
||||
- 验证:单个操作失败只回滚它自己;基准测试给出每秒能提交的写操作数,模拟每秒 200 条消息的写入量时队列不堆积。
|
||||
|
||||
**P3 认证工具 `internal/auth`** · 依赖:T0.1
|
||||
|
||||
- 做:argon2id 哈希池(第 12 节参数、PHC 格式、并发上限);会话令牌和 API 令牌的生成和哈希;锁定计数器(按键计数、时间窗口、到期解除、手动清除)。
|
||||
- 验证:单元测试;并发池能限制同时运行的哈希数;锁定窗口到期自动解除。
|
||||
|
||||
**P4 指标注册** · 依赖:T0.1
|
||||
|
||||
- 做:用 `prometheus/client_golang` 建注册表,定义第 4.3 节列出的指标,给各线打点用;`/metrics` 的处理函数(访问规则由 A 线在路由里加)。
|
||||
- 验证:单元测试能抓到指标文本。
|
||||
|
||||
### 6.3 连接 N
|
||||
|
||||
**N1 端口与 TLS** · 依赖:T0.1
|
||||
|
||||
- 做:第 4.1–4.3 节和第 4.5 节:首字节识别、TLS 和证书每小时重载、`admin_listen` 分离、通道 Listener、路由骨架、受信任代理的真实 IP。
|
||||
- 验证:同一端口上明文 HTTP、TLS+HTTP、裸 TCP、TLS+TCP 都能识别;后台分离时的 404 规则;替换证书后新连接用上新证书;声明 ALPN `mqtt` 的客户端能完成握手。
|
||||
|
||||
**N2 WebSocket 与 mochi** · 依赖:N1、T0.2、T0.4
|
||||
|
||||
- 做:第 4.4 节;第 5 节的装配约束、连接代号、心跳校正、ACL、`OnPublish` 投进每端的串行队列(长度 256,满了形成背压);下行发布(整帧大小检查、QoS 选择、第 7.5 节的大帧并发名额)。
|
||||
- 验证:浏览器页面从别的域名连 `/mqtt` 成功;子协议不对的连接被关闭;超过客户端上限的下行不发布,并回调上层;QoS 0 的事件不占 inflight。
|
||||
|
||||
**N3 登录、会话令牌和握手** · 依赖:N2、P3、T0.3
|
||||
|
||||
- 做:第 5 节「登录与会话令牌」和锁定(用 P3 的工具);内部故障时让 `OnConnect` 返回 error;顶号和连接表;第 6.1 节握手(返回 `session_token`、30 秒没握手就断开、没订阅 down 就断开);`self.logout`;`online_since` 和 `offline_since`;把上下线事件交给 I 线。
|
||||
- 验证:F02 的全部验收(用测试启动器);数据库不可读时客户端连接被直接关闭,而不是收到 `0x86`。
|
||||
|
||||
### 6.4 消息 M
|
||||
|
||||
**M1 提交** · 依赖:T0.2、T0.3、T0.4
|
||||
|
||||
- 做:第 6.2 节和第 7.3 节:先查防重,再校验、查配额、写授权和回复授权,发送时刻已到的立即分发;请求频率桶(第 6.10 节)。
|
||||
- 验证:表驱动测试覆盖防重命中、冲突、配额、授权、延迟和定时互斥。
|
||||
|
||||
**M2 分发与推送** · 依赖:M1、N2
|
||||
|
||||
- 做:第 7.4 节和第 7.5 节:调度器、`queue_full`、停用成员、推送窗口、确认计时和两种超时规则、`OnPublishDropped` 后重推、握手和断线时的投递调整、每秒清理循环。
|
||||
- 验证:F08–F12 的状态机测试和集成测试。
|
||||
|
||||
**M3 确认、撤回、回执、状态** · 依赖:M2
|
||||
|
||||
- 做:第 6.3 节、第 6.4 节、第 7.6 节:确认、撤回结果的判定、回执写入和推送窗口、`revoked`、`status` 分页、收尾删正文、每小时清理(防重行按条件删、分批删、`wal_checkpoint`)。
|
||||
- 验证:F13、F14、F18 的测试。
|
||||
|
||||
**M4 启动恢复** · 依赖:M2
|
||||
|
||||
- 做:第 7.8 节的启动恢复和写库失败处理。
|
||||
- 验证:推送中杀掉进程再重启,不保留的消息在宽限内重连仍能送到;停机期间到点的定时消息在启动后发出。
|
||||
|
||||
### 6.5 身份 I
|
||||
|
||||
**I1 注册接口** · 依赖:T0.3、P3、N1
|
||||
|
||||
- 做:第 6.9 节:开关、安全码、锁定、跨域、字段规则、`source=self`;`settings` 表读写。
|
||||
- 验证:F23 的全部验收。
|
||||
|
||||
**I2 自己的资料和对话密码** · 依赖:N3、T0.2
|
||||
|
||||
- 做:第 6.6 节的 `self.*`(改登录密码时返回新令牌)、`unlock`、授权表(版本、两种授权、回复授权)、按对方计总数的暂停。
|
||||
- 验证:F15 的全部验收。
|
||||
|
||||
**I3 在线与目录** · 依赖:N3
|
||||
|
||||
- 做:第 6.5 节:`presence.get`、`presence.watch`(QoS 0 事件)、`directory.list`(分页和 `query`)。
|
||||
- 验证:F03、F04 的验收。
|
||||
|
||||
**I4 群** · 依赖:M1、I2
|
||||
|
||||
- 做:第 6.7 节全部帧、群事件、成员上限、加人时的对话密码、退群、踢人、解散、转让,以及第 7.6 节表里的作废规则。
|
||||
- 验证:F06、F16 的验收。
|
||||
|
||||
**I5 停用与删除** · 依赖:M3、I4
|
||||
|
||||
- 做:第 7.6 节表里停用、删除端的全部处理(清会话令牌、作废消息、转让群主、清理数据),提供给 A 线调用。
|
||||
- 验证:F01 里停用和删除相关的验收;删除后用同一编号重新开通,不会串到旧数据。
|
||||
|
||||
### 6.6 后台接口 A
|
||||
|
||||
**A1 鉴权** · 依赖:T0.3、T0.4、P3
|
||||
|
||||
- 做:第 8 节的 Cookie 会话、`X-Nixmsg-Request` 头、管理员登录锁定、API 令牌鉴权和权限限制、令牌管理接口、操作日志。
|
||||
- 验证:F17 里令牌相关的验收;用令牌调用改管理员密码和令牌管理接口被拒绝。
|
||||
|
||||
**A2 端管理** · 依赖:A1、I5、N3
|
||||
|
||||
- 做:列表(按来源、在线状态筛选)、开通、CSV 批量开通(整批校验、结果只给一次)、批量停用、启用、删除、编辑、重置密码、踢下线、`unlock`、设置对话密码。
|
||||
- 验证:F01 的验收;导入 1000 行 CSV 成功。
|
||||
|
||||
**A3 其余接口** · 依赖:A1、I1、I4、M3、P4
|
||||
|
||||
- 做:注册设置、群管理、记录查询(筛选、分页、响应不含正文)、概览、只读设置、`/metrics` 的访问规则(第 4.3 节)。
|
||||
- 验证:F17 和 F22 里指标相关的验收;在所有管理接口的响应里搜不到测试正文。
|
||||
|
||||
### 6.7 后台网页 W
|
||||
|
||||
**W1 框架** · 依赖:T0.1、T0.4
|
||||
|
||||
- 做:第 2.4 节的界面要求和 Naive UI 用法;布局、登录页、路由守卫、请求封装(CSRF 头、错误提示)、中文。先按契约用假数据开发。
|
||||
- 验证:`vue-tsc` 类型检查、ESLint、Vitest 通过;页面在 1366×768 和 1920×1080 下没有重叠和遮挡。
|
||||
|
||||
**W2 端管理页** · 依赖:W1
|
||||
|
||||
- 做:列表、筛选、多选批量操作、开通和编辑弹窗、CSV 导入和结果下载、重置密码的结果只显示一次、解除锁定、设置对话密码。
|
||||
- 验证:组件测试;假数据下主流程可用。
|
||||
|
||||
**W3 其余页面** · 依赖:W1
|
||||
|
||||
- 做:概览、注册设置、群、投递记录、API 令牌(创建后只显示一次)、改管理员密码、只读参数。
|
||||
- 验证:组件测试;假数据下主流程可用。
|
||||
|
||||
**W4 联调和端到端测试** · 依赖:A2、A3
|
||||
|
||||
- 做:接真实接口;用 Playwright 走第 13 节的后台主路径(登录、开通、开启注册并设置安全码、建群、看到未完成记录),再加 API 令牌的创建和停用。
|
||||
- 验证:端到端测试在 `task itest` 里通过。
|
||||
|
||||
### 6.8 SDK S1、S2
|
||||
|
||||
S1 先做 Go,再做 JS/TS;S2 先做 Python,再做 Java/Android。每种语言都按下面五个任务做,任务号带语言,例如 `S1-GO-1`、`S2-JAVA-3`。
|
||||
|
||||
| 任务 | 内容 | 依赖 |
|
||||
|---|---|---|
|
||||
| 1 连接和会话 | WebSocket(TCP 可选)、每次连接都带 Clean Start、握手、会话令牌(`onSession`、用令牌重连、`session_invalid`)、退避、停止重连的条件、连接超时 30 秒 | T0.2 |
|
||||
| 2 收发 | 发送队列(上限 1000、在途 100、`rate_limited` 自动重交、`send_at_ms` 不重算)、去重和再确认、串行回调、自动和手动确认、`revoked`、本地大小检查 | 任务 1 |
|
||||
| 3 其余接口 | 撤回、状态、回执、在线、目录、订阅、群、`self.*`、`logout`、注册 | 任务 2 |
|
||||
| 4 接入清单 | 第 9 节的 15 条集成测试,对真实服务端二进制运行 | 任务 3,以及 N3、M3、I2、I4 已合进 main |
|
||||
| 5 文档和示例 | 这种语言的 README 和最小示例 | 任务 4 |
|
||||
|
||||
各语言的要点在 DEVELOPMENT 第 2.3 节和第 9 节:Go 用 `ConnectPacketBuilder` 每次设 Clean Start;JS 同时支持浏览器和 Node,发 ESM 和 CJS,要测跨域;Python 同步为主,另给 asyncio 包装;Java 编译成 Java 8 字节码,写清 Android API 24 的用法。
|
||||
|
||||
### 6.9 测试交付 Q
|
||||
|
||||
**Q1 测试基础设施** · 依赖:T0.5
|
||||
|
||||
- 做:toxiproxy 和 netem 的 Docker 编排(名字带前缀);1000 连接的压测客户端;验收报告生成(F01–F23 对照表)。
|
||||
- 验证:能对一个运行中的服务端注入延迟、丢包、断开;压测客户端能保持 1000 个连接。
|
||||
|
||||
**Q2 验收用例** · 依赖:各线的核心任务
|
||||
|
||||
- 做:PRD 第 10 节每条至少有一个集成测试;各线写过的算数,Q 负责补齐和汇总成报告。
|
||||
- 验证:报告列出 F01–F23 每条的结果。
|
||||
|
||||
**Q3 弱网、崩溃、压测** · 依赖:Q1、Q2
|
||||
|
||||
- 做:DEVELOPMENT 第 13 节的全部相关测试,并出报告。
|
||||
- 验证:报告里的数字满足 PRD 第 8 节。
|
||||
|
||||
**Q4 Docker 与发布** · 依赖:T0.1,最终在阶段 2 完成
|
||||
|
||||
- 做:第 11.4 节的多阶段、多架构镜像;compose 示例;三个平台的二进制;镜像冒烟测试;第 11.3 节 1Panel 证书说明。
|
||||
- 验证:两个架构的镜像都能初始化、启动、通过健康检查。
|
||||
|
||||
**Q5 文档** · 依赖:各线完成
|
||||
|
||||
- 做:README(功能、构建、运行)、运维手册(配置、备份、升级、证书、时钟、指标)、SDK 文档汇总。
|
||||
- 验证:按 README 在一台干净机器上能构建和启动。
|
||||
|
||||
### 6.10 阶段 3:总控 L
|
||||
|
||||
**Z1 全量回归**:在 main 上跑 `task check`、全部集成测试、端到端测试、四个 SDK 的接入清单、弱网和压测。
|
||||
|
||||
**Z2 审核**:按第 7 节清单逐项审核;做安全检查(日志和管理接口里没有正文、密码、令牌);检查依赖的许可证。
|
||||
|
||||
**Z3 交付**:汇总 `docs/DEVIATIONS.md` 并请负责人确认;打 `v0.1.0` 标签并推送;构建二进制和镜像;写交付说明(功能清单、验收结果、已知问题)。
|
||||
|
||||
## 7. 审核清单
|
||||
|
||||
总控每次合并前和最终审核时都要过一遍:
|
||||
|
||||
- 行为和 PRD 一致,实现和 DEVELOPMENT 一致;不一致的已写进 `docs/DEVIATIONS.md`。
|
||||
- 没有违反 DEVELOPMENT 第 15 节的任何一条。
|
||||
- MemRelay 里「依赖库行为核对结论」涉及的点都做对了:`OnConnect` 和 `OnConnectAuthenticate` 的分工、`CodeSuccessIgnore`、下行大小自查、WebSocket 的 Origin 校验、TLS 的 `NextProtos`、每次连接都带 Clean Start。
|
||||
- 所有写库都走写入队列;条件更新都检查了影响行数。
|
||||
- 日志、错误信息、管理接口响应里没有正文、密码、令牌和注册安全码。
|
||||
- 新代码有测试;测试用随机端口和临时目录。
|
||||
- 没有引入技术栈以外的框架和中间件。
|
||||
|
||||
## 8. 负责人操作说明
|
||||
|
||||
用本地 Agent。仓库在自建的 Gitea 上,Cursor 的云端 Agent 只支持 GitHub、GitLab 等托管平台,而且用不了本机的 Docker。
|
||||
|
||||
1. 按 `Ctrl+Shift+P`,执行 `Open Agents Window` 打开代理窗口。每新建一个 Agent,先在输入框的模型选择器里选好 Grok 模型再发第一条消息。
|
||||
2. 阶段 0:新建一个 Agent 当总控,就在主工作区 `e:\code\NixMsg` 里运行,发「总控提示词」。等它报告阶段 0 已经合进 main 并推送。
|
||||
3. 阶段 1:每条线新建一个 Agent,让它在自己的工作树里运行(在代理窗口新建时选工作树;找不到这个入口就让 Agent 按第 4.1 节自己建)。发「开发线提示词」,把 `<线名>` 换成 P、N、M、I、A、W、Q。建议同时跑的不超过 7 个;S1、S2 在机器有余力时再开,或者等 N、M 合进 main 后再开。
|
||||
4. 日常:在代理窗口侧栏看各 Agent 的进度和改动。某条线说可以合并时,告诉总控「合并 <分支名>」,或者让总控定期查 MemRelay 里标了 `ready-to-merge` 的检查点。Agent 提问时在它的对话里回答,拿不准的转给总控判断。
|
||||
5. 阶段 2、3:N、M、I、A 的核心任务合进 main 后,让 S1、S2、W、Q 按任务表联调和验收;最后告诉总控「开始阶段 3」。
|
||||
6. 注意:Cursor 默认整台机器最多保留 25 个工作树,超出会自动删掉最旧的,所以要各 Agent 勤提交、勤推送;需要时在设置里调大 `cursor.worktreeMaxCount`。
|
||||
|
||||
总控提示词:
|
||||
|
||||
```text
|
||||
你是 NixMsg 的总控 Agent,在主工作区 e:\code\NixMsg 工作。先按协作规则读 MemRelay 项目记忆,再完整阅读 docs/PRD.md、docs/DEVELOPMENT.md、docs/TASKS.md。现在完成 TASKS.md 阶段 0 的 T0.1 到 T0.5:每个任务一个分支,一功能一验证一提交,全部验证通过后快进合并进 main 并推送。完成后向我汇报,并告诉我可以开启阶段 1。之后你负责按 TASKS.md 第 4.3 节和第 7 节审阅、合并各线分支并跑集成验证,最后执行阶段 3。
|
||||
```
|
||||
|
||||
开发线提示词:
|
||||
|
||||
```text
|
||||
你是 NixMsg 的开发线 <线名> Agent。先按协作规则读 MemRelay 项目记忆,再阅读 docs/TASKS.md(重点第 3、4、5 节和第 6 节里 <线名> 的任务)以及 PRD、DEVELOPMENT 的相关小节。在你自己的工作树和 feat/<任务号>-<简述> 分支上工作,只改本线负责的目录,按依赖顺序完成本线任务:一功能一验证一提交,并推送自己的分支。每个任务完成后 rebase 到 origin/main、跑全部验证,通过后在 MemRelay 存标签为 ready-to-merge 的检查点,然后告诉我。缺工具用 scoop 安装;文档有疑问先问我,不要自己改产品行为。
|
||||
```
|
||||
|
||||
## 9. 负责人已确认的协作授权
|
||||
|
||||
- 2026-09-30:允许各开发 Agent 在 rebase 后用 `git push --force-with-lease` 更新自己的 `feat/*`、`fix/*` 远端分支;`main` 任何时候都不许强推。
|
||||
Reference in New Issue
Block a user