NullChat 使用说明书(客户端 + 服务器端)

编写日期:2026-10-12 | 适用版本:客户端 0.1.0(Flutter 3.47.1)· 服务端 nullchat-server

编写依据:逐条对照当时的源码(页面文件、router.go、config.go),不写代码里没有的东西。

读法建议:只想用 App ⇒ 读第一部分;要自己搭服务器 ⇒ 读第二部分。

⚠️ 本文与实现可能随时间漂移。发现不符时以代码为准,并按《大更新流程》同步本文。


第一部分 客户端(Android App)

0. 名词与总览

名词含义
注册 ID你的账号名(3–32 位,字母开头,可含字母/数字/下划线)。创建后不可修改
昵称对方看到的名字(可改;暂不支持头像)
安全码一串用于确认"没有中间人"的号码;必须通过其它渠道与对方核对
端到端加密消息在你手机上加密后才发出;服务器只有密文
阅后即焚对方读到后约 5 秒,双方本机与服务端密文都会被清除
会话一条聊天记录(单聊或群聊)

界面总览:底部有 4 个标签 —— 消息 / 群聊 / 联系人 / 我。


1. 首次启动

1.1 引导页(Onboarding)

按键作用
我有服务器地址进入服务器设置页,填写你自己的(或朋友给你的)服务器地址
先去登录直接去登录页(服务器地址已填过时用)

引导页展示三条产品承诺:"消息在你手机上加密后才发出去"、"你需要一台自己的服务器"、"没有广告、没有推荐"。

1.2 服务器设置页

按键 / 字段作用说明
服务器地址 输入框填 主机:端口(例如 chat.example.com:443)只填域名/IP + 端口,不要带 https://(界面会校验)
保存保存地址并校验连通性保存失败会给出原因
查看详情展开诊断信息(含实际使用的三个接口地址,等宽字体)排查"连不上"时用
高级设置 开关打开后可手动指定三个接口地址仅用于非标准端口或自定义路径;普通用户不要动

2. 登录 / 注册 / 找回密码

2.1 登录页

按键 / 元素作用
注册ID 输入框填你的注册 ID
密码 输入框填密码
眼睛图标(tooltip:显示密码 / 隐藏密码)切换密码明文显示
登录(大按钮)登录
服务器设置(右上角)改服务器地址
注册账号去注册页
忘记密码去找回密码页
底部服务器行(服务器:host:port)点它直接去服务器设置;未设置时显示"还没有设置服务器,点这里填写"
去设置服务器(弹窗内)未填地址就点登录时的引导按钮

2.2 注册页

按键 / 字段作用 / 规则
注册ID3–32 位,字母开头,可含字母/数字/下划线
密码至少 10 位,需含大小写字母和数字
眼睛图标显示 / 隐藏密码
昵称对方看到的名字(暂不支持头像)
邮箱仅用于找回密码(务必填对)
注册 NullChat提交注册
已有账号?去登录返回登录页

2.3 找回密码页

按键 / 字段作用
注册邮箱填注册时用的邮箱
邮箱验证码收到的验证码(有发送频率限制)
新密码至少 10 位,含大小写字母和数字
眼睛图标显示 / 隐藏密码
找回密码提交重置

2.4 修改密码页(两种入口)

按键 / 字段作用
当前密码 / 临时密码(当前密码)后者出现在"管理员重置密码后强制改密"的情形
新密码 / 确认新密码至少 10 位,含大小写字母和数字
显示密码切换明文显示
修改密码提交。成功后所有设备都需用新密码重新登录
先跳过(稍后再改)仅在主动改密入口出现;强制改密流程里不给跳过

2.5 安全锁页(PIN)

按键 / 字段作用
PIN / 再次输入 PIN设置或确认 PIN
指纹快捷解锁 开关允许用指纹/人脸快捷解锁(PIN 仍始终可用,指纹失败时不会把用户锁在外面)
开启 / 关闭安全锁开关安全锁。关闭时需先输入当前 PIN
解锁界面上的 PIN 输入 + 指纹按钮启动 App 时解锁

PIN 由加盐哈希保存;连续输错会节流(防暴力破解)。


3. 「消息」标签(会话列表)

按键 / 元素作用
⊕ 添加联系人(右上角)去「添加联系人」页
点某条会话进入聊天
长按某条会话弹出「删除会话」确认框(取消 / 删除)。仅删除本地会话记录
下拉刷新会话列表
空态按钮「添加联系人」没有会话时的引导按钮

3.1 「过期未收提醒」行(重要)

有人给你发消息、而你当时不在线 ⇒ 消息进入服务端 24 小时加密缓存;到期清理后, 服务端只保留一条元数据占位:发送者 | 时间 | N 条消息未收到,已过期删除。 它不含任何正文,也不可打开(灰色、无箭头)。

操作效果
点一下这一行消除:立刻从列表移除,并同步删除服务端那条记录(换设备/重登也不会再出现)
不点它服务端最多保留 7 天,到期自动清理

⚠️ 刻意没有"按时间自动隐藏":那会让你在没看到时就错过这条信息。


4. 聊天页(按键最密集,重点)

4.1 标题栏

按键作用
← 返回(tooltip:返回)回到上一页
头像 + 名字 / 群名(点标题)单聊:进入该好友的资料页(那里有安全码);群聊:进入群资料页
⋯(tooltip:好友资料与安全码)单聊:进好友资料页
⋯(tooltip:群资料)群聊:进群资料页(群里没有"peerID",因此这里不冒充"好友资料")

4.2 常驻提示条(仅在需要时出现)

对方加密身份变更后,会话顶部会出现一条红色提示条(不随消息滚动消失):

按键作用
我已核对你已在其它渠道核对过安全码 ⇒ 关闭这条提示(同时把它记为"已读")

4.3 消息气泡

操作效果
长按自己的/对方的气泡弹出菜单:复制 / 删除 / 取消
复制把该条文本复制到剪贴板(提示"已复制")
删除二次确认后只从你手机上删除,对方仍能看到(E2EE 下做不到"撤回")
「已读」标记单聊里,你发出的最后一条消息在对方阅读后显示「已读」(受对方「发送已读回执」开关控制)。退出会话再进来仍会保留(记录在本机;换手机/重装后不再显示,与旧消息读不出来同理)
「已焚毁」阅后即焚消息焚毁后的痕迹(占位提示,不可打开)
加密信息条目会话内的系统类提示(如安全码变更记录)

4.4 输入栏(一个按钮,两种发送)

元素手势作用
输入框输入多行文本(1–4 行自动增高);键盘回车键=发送
发送(默认外观)点击发送普通消息
发送长按切换为阅后即焚模式(切换时会有一条提示;发送成功不再提示)
焚毁(红色 + 🔥 图标)点击发送阅后即焚消息:对方读到后约 5 秒自动焚毁
焚毁长按切回普通发送

模式是可见的:一旦进入焚毁模式,按钮立刻变成红色并显示 🔥「焚毁」——

你不看提示也能知道"现在按下去会发什么",避免误发。

阅后即焚的边界(务必知道):焚毁从"对方读到"开始计时;

对方若是截图、或在你发消息时离线(消息进服务端缓存),行为按上面的规则处理;

焚毁会清理本机明文缓存 + 对端明文缓存 + 服务端密文缓存三处。


5. 「群聊」标签

按键作用
⊕ 创建群聊(右上角)去创建群聊页
点某个群进入群聊
空态按钮「创建群聊」没有群时的引导按钮

5.1 创建群聊页

按键 / 字段作用
群名称填群名(界面提醒:不要把私密信息写进群名)
选择成员从好友里勾选成员
创建群聊创建(无好友时提示"还没有好友,先添加好友才能建群")

5.2 群资料页

按键作用
群成员 列表查看成员(显示人数)
邀请成员从好友里勾选并邀请(无可邀请好友时给出空态)
解散群聊(群主)二次确认后解散(解散)
退出群聊(非群主)二次确认后退出(退出群聊)
取消(确认框)什么都不做

群聊的加密方式:逐成员扇出(给每个成员单独加密一份),不是组密钥。


6. 「联系人」标签

按键作用
⊕ 添加联系人(右上角)去「添加联系人」页
新的朋友(带数字角标)查看好友申请(角标=待处理数量)
点某个好友进入好友资料页
长按某个好友弹出「删除好友」确认框(取消 / 删除):删除后需重新添加才能聊天
空态提示去「添加联系人」用注册 ID 搜索

6.1 添加联系人页

按键 / 字段作用
输入对方注册ID填对方 ID(顶部也会显示你自己的 ID,方便对方加你)
搜索搜索该 ID
添加好友发起好友申请
验证消息申请附言(界面明确提醒:验证消息不加密,勿填敏感信息)
发送发送申请
取消关闭填写框

6.2 新的朋友页

按键作用
同意通过申请(成为好友)
拒绝拒绝申请
空态"暂无新的好友申请",并提示可去「添加联系人」搜索

6.3 好友资料页

按键 / 元素作用
ID: xxx对方注册 ID(不可改)
备注名改备注(仅自己可见,但服务器可见——界面已注明)
安全码进入安全码核对页("核对后可以确认没有人在中间偷看你们的消息")
发消息直接进入与该好友的聊天
删除好友二次确认后删除(取消 / 删除)

7. 「我」标签

页面自上而下:ID + 复制按钮,然后6 个入口。

#入口作用
—⧉ 复制按钮(ID 右边)复制你的注册 ID 到剪贴板(提示"已复制注册ID")
1个人资料查看注册 ID / 昵称 / 邮箱绑定状态 / 注册时间 / 最后登录;每行右侧有复制按钮
2修改密码自助改密(见 §2.4)
3端到端加密加密说明页(做到了什么 / 做不到什么)
4设备管理查看登录过本账号的设备并踢出
5设置见 §8
6退出登录(红色)退出并回到登录页(见 §10.3)

7.1 设备管理页

按键 / 元素作用
本机设备(本机) 行当前这台设备
其它设备行同一账号登录过的其它设备
退出本机登录(tooltip,本机那行的图标)本机退出登录
踢出「设备名」(tooltip,其它设备行的图标)二次确认后把该设备踢下线
取消 / 踢出(确认框)确认或取消
空态"没有其它设备"(只显示本机时)

每台设备有一个本机稳定 ID:同一台机器永远复用同一个 ID,因此正常使用不会产生重复设备行;

只有卸载/清除数据后重新登录,才会被记为一台"新设备"。

7.2 安全码页

按键 / 元素作用
你的安全码 + 一串号码等宽显示,供核对
复制复制安全码
刷新重新获取/计算
数字一样,标记为已验证你已在其它渠道核对一致 ⇒ 标记为已验证
取消已验证标记撤销上面的标记
重试加载失败时重试

⚠️ 核对必须走其它渠道(当面、电话、另一款工具)。不要把安全码发到本 App 的聊天里核对——

那等于用"可能已被劫持的通道"去验证这个通道本身。

7.3 高安全模式说明页

纯说明页(无操作按钮),六节:它防的是什么 / 两种模式的行为差别 / 为什么默认是「关」/ 怎么核对安全码 / 那我该开还是关 / 两个常见疑问。


8. 设置页(逐项)

项类型作用
服务器设置入口改服务器地址(同 §1.2)
通知内容预览开关(默认关)打开后通知栏/锁屏会显示消息内容;关闭时只显示"您有 N 条新消息"。关闭状态绝不包含正文
安全锁开关启动 App 需输入 PIN(见 §2.5)
高安全模式开关(默认关)见下
高安全模式说明入口进说明页(见 §7.3)
后台保活开关(默认关)开启后前台服务保持连接以即时收消息;代价是一条常驻通知(系统强制,无法隐藏)
打开电池优化设置入口(仅保活开启且未加白名单时出现)跳到系统电池优化页,把 NullChat 加入白名单
发送已读回执开关(默认开)关掉后对方看不到你的已读状态(注意:已读记录仍由服务器产生,只是不显示给对方)

8.1 「高安全模式」到底做什么

模式遇到"对方加密身份变了"时的行为
关(默认)自动接受新身份(消息立刻恢复),并在会话里留一条显眼的红色提示条;你可以在提示条上点「我已核对」
开不自动接受,弹对话框要你亲自确认;对话框给出对方当前安全码,并有三个动作:接受 / 取消 / 「对方重装过 → 接受并重建」

为什么默认是"关":接受新身份是人的决定,程序不能替你点。默认开着会导致 "对方换手机后你这边永久读不出消息"(实测过,且重启也好不了)。 默认关 = 先让消息通、再显眼提示(与主流做法一致);代价是"自动信任"。

高安全模式的代价:对方每次换机/重装,你都要手动点一次;不点就一直显示「无法解密」。


9. 通知与后台

主题说明
通知内容默认不预览(见设置页「通知内容预览」)。锁屏/通知栏只显示"您有 N 条新消息"
后台保活默认关。开启后有一条常驻通知;建议同时加入电池优化白名单(设置页有直达入口)
阅后即焚的预览会话列表对该类消息一律显示 [阅后即焚],不会泄露正文

10. 隐私与安全操作

10.1 核对安全码(防中间人)

  1. 聊天页 → ⋯ → 好友资料页 → 安全码(或直接进入安全码页);
  2. 两人各自看到一串号码,通过其它渠道(当面/电话/另一款工具)逐位核对;
  3. 完全一致 ⇒ 点「数字一样,标记为已验证」;
  4. 不一致 ⇒ 有人在中间,不要继续在该会话里发送敏感内容。

10.2 阅后即焚怎么用

长按发送按钮 ⇒ 按钮变红显示 🔥焚毁 ⇒ 再点击即发送。 对方读到后约 5 秒焚毁;焚毁后双方都只剩「已焚毁」痕迹。

10.3 「退出登录」会清掉什么

退出登录(「我」→ 退出登录)会:断开连接、取消通知、清除本机 E2EE 身份与会话材料、 清除明文缓存、清除登录 token。后果:

10.4 换手机 / 重装后会发生什么

场景结果
你换了手机新手机上旧消息读不出来(必然代价);对方会看到"你的安全码已变更"并(默认)自动恢复通信;此后新消息正常
对端换了手机你这边默认会自动接受新身份并留下红色提示条;建议点「我已核对」并核对安全码
双方都重装互发一条消息即可恢复(默认模式下无需人工点击)

第二部分 服务器端

11. 组成与端口(线上实测)

组件容器(实测名)作用
自研服务nullchat-server注册/登录/改密/设备/提醒/E2EE 预密钥/焚毁清理;对外只监听本机端口,由 nginx 反代
数据库nullchat-mongodb账号、设备、占位提醒、审计;不含消息正文
缓存nullchat-redisOpenIM token、离线密文缓存、消息缓存(AOF 已开启)
反向代理nullchat-nginxTLS 终止 + 反代(对外 https://域名:443)
OpenIMopenim-*(server/api/msggateway/msgtransfer/rpc-*/push 等 + nullchat-etcd)消息收发管道

deploy/nullchat-compose.yml 是片段(只含自研服务);线上实际编排以服务器 /opt/nullchat/deploy/ 为准。

12. 首次部署(要点)

  1. 准备:一台 Linux 服务器 + 域名 + 证书;Docker 与 compose。
  2. 取源码:把 server/ 上传到服务器(例如 /opt/nullchat/nullchat-src)。

🔴 务必先校验目录:head -1 go.mod 必须是 module nullchat/server,否则说明传错地方了。

  1. 写 .env(与 compose 同目录):至少设置

NULLCHAT_JWT_SECRET、NULLCHAT_ADMIN_TOKEN(两个都不能用默认值 change-me-in-prod)、 NULLCHAT_MONGO_URI、NULLCHAT_REDIS_ADDR/PASS、NULLCHAT_OPENIM_*、SMTP 相关(要用找回密码就必须配)。

  1. 建镜像:docker build -t nullchat-server:<标签> /opt/nullchat/nullchat-src。

⚠️ compose 里是 image: + read_only: true(没有 build:)⇒ 改代码必须重建镜像再 docker compose up -d --force-recreate nullchat-server;容器内直接换二进制不行(rootfs 只读)。

  1. 起服务:先 Mongo/Redis/etcd/OpenIM,最后自研服务与 nginx。
  2. 自检:GET /healthz 应为 200;随便打一个受保护接口应为 401(而不是 404/500)。

13. 日常运维

目的命令(示例)
看容器状态sudo docker ps --format '{{.Names}}\t{{.Status}}\t{{.Image}}'
看自研服务日志sudo docker logs --tail 200 nullchat-server
健康检查curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<端口>/healthz
进 Mongosudo docker exec -it nullchat-mongodb mongosh "mongodb://<用户>:<口令>@127.0.0.1:27017/?authSource=admin"
进 Redissudo docker exec -e REDISCLI_AUTH="$PASS" nullchat-redis redis-cli
重启自研服务sudo docker compose -f <compose> up -d --force-recreate nullchat-server

🔴 两条纪律:

  1. Mongo 必须显式选库:db.getSiblingDB('nullchat') —— URI 不带库名时 mongosh 会落在 test 库(会查出与服务端不符的"0 行")。
  2. 不要把口令放进命令行:用环境变量(如 REDISCLI_AUTH)传入,且不要用 2>/dev/null 藏错误。

14. 配置项(internal/config/config.go,节选)

环境变量默认说明
NULLCHAT_HTTP_ADDR:8080监听地址(线上由 compose 覆盖)
NULLCHAT_MONGO_URI / _DBmongodb://127.0.0.1:27017 / nullchat数据库
NULLCHAT_REDIS_ADDR / _PASS / _DB127.0.0.1:6379 / 空 / 0缓存
NULLCHAT_JWT_SECRETchange-me-in-prod🔴 必须改
NULLCHAT_ADMIN_TOKENchange-me-in-prod🔴 必须改(管理接口口令)
NULLCHAT_ACCESS_TTL_HOURS / _REFRESH_TTL_HOURS168 / 720token 有效期
NULLCHAT_EMAIL_AES_KEY空邮箱字段加密密钥
NULLCHAT_SMTP_HOST/PORT/USER/PASSWORD/FROM空 / 465 / 空 / 空 / NullChat 系统找回密码邮件;不配则找回功能不可用
NULLCHAT_OPENIM_BASE_URL / _SECRET / _ADMIN_IDhttp://127.0.0.1:10002 / 上游默认 / imAdminOpenIM 对接
NULLCHAT_REGISTER_LIMIT_PER_IP50注册限速
NULLCHAT_LOGIN_MAX_FAILS / _LOCK_MINUTES5 / 15登录失败锁定
NULLCHAT_SEND_CODE_LIMIT_SECONDS60验证码发送间隔
NULLCHAT_RESET_VERIFY_MAX_TRY / _SEND_LIMIT_PER_IP_HOURLY / _ATTEMPT_LIMIT_PER_IP_HOURLY5 / 10 / 30找回密码限速
NULLCHAT_OFFLINE_CACHE_TTL_HOURS24离线密文缓存 TTL
NULLCHAT_PLACEHOLDER_TTL_DAYS7过期未收提醒的保留天数(2026-10-12 由 30 缩短)
NULLCHAT_PURGE_INTERVAL_MINUTES1焚毁/清理扫描间隔

15. 自研服务端接口一览(internal/api/router.go 原文)

方法路径鉴权作用
POST/api/v1/auth/register无注册
POST/api/v1/auth/login无登录
POST/api/v1/auth/send_reset_code无发送找回验证码
POST/api/v1/auth/reset_password无用验证码重置密码
GET/api/v1/user/devices用户 token设备列表
POST/api/v1/user/devices/kick用户 token踢出设备
GET/api/v1/user/notices用户 token过期未收提醒列表
DELETE/api/v1/user/notices/:convID用户 token消除一条提醒(2026-10-12 新增,按 owner_id+conv_id 删除)
POST/api/v1/user/im_token用户 token重新签发 OpenIM token(自助恢复)
POST/api/v1/user/password用户 token修改密码
GET/api/v1/user/password/status用户 token是否需要强制改密
POST/api/v1/user/password/verify用户 token校验当前密码
GET/api/v1/user/profile用户 token个人资料
POST/api/v1/e2ee/prekeys用户 token上传预密钥包
POST/api/v1/e2ee/prekeys/consume用户 token消费(取用)对方预密钥
GET/api/v1/e2ee/prekeys/:userID用户 token查询对方预密钥
POST/api/v1/e2ee/burn用户 token立即删除服务端密文缓存(阅后即焚用;{peerID 或 groupID, seq})
POST/api/v1/admin/user/ban / unban管理 token封禁 / 解封
POST/api/v1/admin/user/reset_password管理 token管理员重置密码
GET/api/v1/admin/audit管理 token审计查询
GET/healthz无健康检查

16. 备份与恢复

对象做法
MongoDBmongodump(含 nullchat.users/devices/expired_notices/admin_audit_logs 等)
Redis已开 AOF(重启后 token 与离线密文缓存可存活)
服务端源码/镜像部署前 tar 备份源码 + 备份旧镜像标签(每次都留,回滚一条命令)
恢复顺序Mongo → Redis → OpenIM → 自研服务 → nginx

17. 故障排查

现象先查什么常见根因
消息发出去对方收不到OpenIM 的 msgtransfer/msggateway/rpc-msg/push 是否都在跑;MQ lag/pending管道卡住(重启相关容器)
一直"无法解密"是否单侧重装过;日志有无 UntrustedIdentity身份变更未被接受(默认模式会自动接受;高安全模式需手动确认)
设备列表出现重复nullchat.devices 里同 user_id 有多少行旧客户端用 deviceID: null(现已修);历史行可清理(保留 last_login_at 最新一条)
提醒一直不消失expired_notices 是否有该 owner_id+conv_id客户端未点击(点一下即消除);或保留期未到(最长 7 天)
登录页反复出现token 是否过期;POST /user/im_token 是否可用token 失效(限时/被踢);可用自助换 token

18. 安全运维红线

  1. 消息正文永不落库:openim_v3.msg = 0;离线只缓存密文(24h)。
  2. 日志不打正文(含依赖层拦截器);新增日志只允许"计数/ID/状态"。
  3. 通知默认不预览。
  4. 凭据不进 git:调试服务器信息*.txt 已被 .gitignore;提交前必须按《大更新流程》§3.1 做凭据扫描

(含日期快照——版本快照/ 是 git 跟踪目录)。

  1. 用户触发的删除必须校验归属(如提醒消除走 owner_id + conv_id)。

19. 简明速查(打印版)

【最简单的用法】
1. 装 App → 引导页点「我有服务器地址」→ 填 host:port → 保存
2. 没有账号:登录页「注册账号」→ 填 ID/密码/昵称/邮箱 → 注册
3. 加好友:消息页右上角 ⊕ → 填对方注册 ID → 搜索 → 添加好友(附验证消息)
   对方在「联系人 → 新的朋友」点「同意」
4. 聊天:消息页点会话 → 输入 → 点「发送」
5. 阅后即焚:长按「发送」→ 按钮变红🔥「焚毁」→ 再点发送(对方读到后约 5 秒焚毁)
6. 复制某条消息:长按气泡 → 复制
7. 防止中间人:⋯ → 好友资料 → 安全码 → 用别的渠道核对 → 标记已验证
8. 改服务器:登录页右上「服务器设置」或 我 → 设置 → 服务器设置