本地存储与离线时长
本页说明两件事:各端 SDK 把哪些数据落在本地(落在哪里、能否清理),以及用户离线期间的消息能在服务端保留多久、重连时补发多少。融云通过控制台配置“离线消息存储时长”;本项目没有按时长计费的离线存储,离线补发由“未 ACK 游标 + 历史清理水位 + 数据保留”共同决定。
概念
| 概念 | 本项目行为 | 是否可配置 |
|---|---|---|
| 本地消息库 | Android / Electron 落 SQLite;Web / Flutter / Native / Node 仅内存 | Android/Electron 由 SDK 内部管理,无开关 |
| 本地会话状态 | 未读、置顶、免打扰、草稿;Android/Electron 持久化,Web 仅内存(IMKit.store 可另存) | Web 由宿主决定是否用 IMLocalStore |
| 离线补发 | 服务端按 {app}:{user}:{conversation} 的 deliveredSeq 重放 | 不可配置;由 ACK 与清理水位决定 |
| 离线存储时长 | 没有按天/按小时的离线存储时长配置项 | —(融云有该配置,本项目未实现) |
| 历史保留 | 消息存于服务端状态库,可调用 message.history.clean 按会话/时间清理 | 按接口调用清理,不是 TTL |
| 聊天室 | 不落库、不补发、不可本地持久化(断连即离开) | 协议固定语义 |
融云按 App / 用户配置离线消息存储时长(例如 7 天),过期后不再补发;本项目没有时长配置,也没有过期淘汰任务。
本项目的补发窗口 = “未被 ACK 且未被 message.history.clean 清理水位覆盖”的全部消息。要控制窗口,请使用清理接口或让客户端及时 ACK。
协议声明 offlineMaxPerConversation=500,当前服务端补发实现未按 500 条截断;单会话超过 500 条未 ACK 时会全部重放,客户端应改用 REST 历史分页兜底。
前置条件
- 已了解各端 SDK 的初始化方式(见「客户端集成」)。
- 验证清理行为需要服务端 Admin Secret(
IM_ADMIN_SECRET,本地默认dev-admin)。 - Web 端验证需要浏览器允许
localStorage(无痕/受限环境会自动降级为内存,刷新即丢失)。
步骤
- 确认当前端的存储实现
按下表核对 SDK 的实际存储位置;不是所有端都有本地库,不要把 Web/Flutter 当成可离线持久化。
- 验证落库(Android / Electron)
Android:登录并收发若干消息后,应用私有目录下出现
nipaas_im_<appkey>.db;messages表含owner_id列。Electron:主进程
dbPath下出现im-electron/<appkey>/<userId>.db;IMClient.localDb.listConversations()能读到会话。 - 验证内存态(Web / Flutter)
Web:登录后刷新页面,会话列表需要重新走
getConversationList();如果宿主没有用IMKit.IMLocalStore保存会话,界面上不会保留任何消息。Flutter:热重启(hot restart)后客户端实例重建,
getHistoryMessages只能从服务端拉取。 - 验证离线补发与游标
用同一账号建连,下线期间由另一账号发送 3 条消息,再重连:应依次收到
hello(含syncState.deliveredSeq)、3 条message、sync_done{count:3}。对其中 1 条发送
ack后断开再重连,count应减少为 2 —— 游标已推进。 - 清理本地与服务端数据
Android:
deleteConversation(conversationType, targetId, callback)删除会话行(历史消息保留);SDK 未暴露“清空本地库”公开方法,需要彻底清空时由宿主清除应用数据(卸载/系统“清除数据”),本地库按owner_id隔离,换账号不会串数据。Electron:主进程
client.localDb.clearAll()、clearMessages(conversationType, targetId)、deleteMessagesByTime(conversationType, targetId, untilTime)。Web:
IMKit.store.clear()只清 IMKit 命名空间;IMLib 的内存态随页面销毁。服务端:
POST /im/v2/message/history/clean(带签名)清理指定会话,或带userId只清理该用户视角。
参数表
各端本地存储事实
| 平台 | 存储介质与位置 | 持久化内容 | 重启/刷新后 | 清理方式 |
|---|---|---|---|---|
| Web(IMLib) | 进程内存(Map),无 IndexedDB/localStorage | 会话缓存、未读、消息状态、去重集合 | 全部丢失,依赖 REST 重新拉取 | 页面销毁即释放 |
| Web(IMKit.store) | localStorage,键前缀 imkit:;不可用时降级内存 Map | 宿主主动保存的会话快照、草稿、主题偏好(不是消息库) | localStorage 保留;降级模式丢失 | IMKit.store.clear() / remove(key) |
| Electron | node:sqlite DatabaseSync;<dbPath>/im-electron/<appkey>/<userId>.db | 消息、会话、草稿、去重索引、未读(localDb 26 个方法) | 保留(重启后可直接读本地库) | localDb.clearAll/clearMessages/deleteMessagesByTime |
| Android | 系统 SQLite(SQLiteOpenHelper);库名 nipaas_im_<appkey>.db | schema v2:messages / conversations / sync_cursors / kv,按 owner_id 隔离 | 保留(进程重启后可直接读) | 公开 API 仅 deleteConversation(删会话行、保留消息);无公开“清空本地库”方法 |
| Flutter | 客户端实例内存(_messageCache) | 消息缓存、回应/扩展缓存 | 丢失(实例重建) | 实例销毁;本地持久化为待补项 |
| Native(C ABI) | 无 | 仅通过 imsdk_client_poll 返回帧 | 丢失 | — |
| Node SDK | 无(内存) | 连接态与回调 | 丢失 | 进程退出 |
| iOS / HarmonyOS | 未实现(契约同构) | 无运行时代码 | — | — |
离线补发相关参数
| 参数 | 值 | 来源 | 说明 |
|---|---|---|---|
| 单会话离线消息上限(条) | 500 | protocol.json limits | 协议声明的单会话离线补发上限;当前服务端未按此值截断 |
| 历史消息默认分页大小 | 20 | protocol.json limits | getHistoryMessages / message.history 默认分页大小 |
| 历史消息最大分页大小 | 100 | protocol.json limits | 单页最大条数;超过 500 条未 ACK 时用该分页兜底 |
deliveredSeq | 每用户每会话一个 | Server.delivered | ack 帧推进;hello.syncState 返回当前值 |
historyCleared 水位 | 毫秒时间戳 | Server.history_cleared | message.history.clean 写入;补发与历史读取同时过滤 |
| Token 有效期 | 7 天 | TOKEN_TTL_MS | 离线期间 Token 过期需重新签发后才能重连 |
帧与传输上限见 限频、配额与容量限制;补发时序见 多端在线与多设备同步。
示例
查看连接与消息(服务端视角)
# 连接明细中的 lastHeartbeatAt 可判断端是否仍在线;消息是否补发由 deliveredSeq 决定
curl -sS "http://127.0.0.1:8080/api/admin/apps/demo/connections?userId=alice" \
-H 'X-Admin-Secret: dev-admin'
# 查看会话未读数(未读与 deliveredSeq 是两套状态:未读面向 UI,游标面向补发)
curl -sS "http://127.0.0.1:8080/api/admin/apps/demo/messages?userId=alice&conversationType=1&targetId=bob" \
-H 'X-Admin-Secret: dev-admin'清理服务端历史(保留最近消息)
// clean-history.mjs — 清理指定会话在某个时间点之前的历史(Open API 签名)
import { createHash, randomBytes } from 'node:crypto';
const HOST = 'http://127.0.0.1:8080';
const APP_KEY = 'demo'; // 变量:应用 AppKey
const APP_SECRET = process.env.APP_SECRET || 'YOUR_APP_SECRET'; // 变量:应用 AppSecret
const nonce = randomBytes(8).toString('hex'); // 8-64 字符
const timestamp = String(Date.now()); // 允许 ±300000ms
const signature = createHash('sha1').update(APP_SECRET + nonce + timestamp).digest('hex');
const res = await fetch(HOST + '/im/v2/message/history/clean', {
method: 'POST',
headers: {
'content-type': 'application/json',
'App-Key': APP_KEY,
Nonce: nonce,
Timestamp: timestamp,
Signature: signature,
},
// beforeTimestamp 省略表示清理全部;userId 省略表示全局清理
body: JSON.stringify({ conversationType: 1, targetId: 'bob', beforeTimestamp: 1750000000000 }),
});
console.log(res.status, await res.text());清理本地数据(Electron / Android)
import electronMain from '@ni-paas-im/electron/main';
import { app } from 'electron';
// localDb 只在主进程(或经 preload 白名单代理)可用;聊天室会话不落本地库。
const client = new electronMain.IMElectronClient({
appkey: APPKEY,
serverUrl: 'http://127.0.0.1:8080',
dbPath: app.getPath('userData'),
});
await client.connect(TOKEN);
// 只清某个会话的消息,保留会话行
await client.localDb.clearMessages(1, 'bob');
// 清到指定时间点之前
await client.localDb.deleteMessagesByTime(1, 'bob', Date.now() - 7 * 24 * 3600 * 1000);
// 登出/换账号:清空当前用户全部本地数据
await client.localDb.clearAll();// 初始化(幂等):同一 appkey 返回同一实例,内部只持有 applicationContext。
IMClient client = IMClient.init(context,
new IMClientOptions("demo", "http://127.0.0.1:8080"));
// 删除会话记录(历史消息保留;重复删除同样成功,回调 false)。
client.deleteConversation(ConversationType.PRIVATE.getCode(), "bob", (ok, err) -> {
// ok=true 表示本次真的删除了记录
});
// 本地库无公开“清空”方法:彻底清理请由宿主清除应用数据(或卸载重装)。常见问题
Web 端能做离线消息本地缓存吗?
IMLib 不做,刷新即丢。宿主可以用 IMKit.IMLocalStore 自行保存“会话列表快照 + 草稿 + 主题”等非敏感数据,但它不是消息库;消息仍需通过 getHistoryMessages 从服务端拉取。
Android / Electron 的本地库会无限增长吗?
不会自动淘汰。SDK 提供删除/清空接口(Electron localDb.clearMessages/deleteMessagesByTime/clearAll;Android 只有 deleteConversation),需要宿主按产品策略(例如保留 30 天)定期调用或自行清理数据库。
离线多久之后收不到消息?
没有时间阈值。只要消息仍在服务端、且未被 message.history.clean 清理、且该用户该会话的 deliveredSeq 未推进,重连就会补发。Token 过期(7 天)会先导致建连失败。
为什么协议写了 500 条上限,实际补发更多?
500 是 IDL 声明值,服务端补发实现未做截断。这是已知实现差异;客户端应按“收到 sync_done 后用 REST 历史分页对齐”的方式编写,不要依赖补发条数。
聊天室消息能补发吗?
不能。聊天室消息不持久化、不补发,断连自动离开;Electron 的 localDb 也会拒绝聊天室会话(isPersistentConversationType 为 false)。需要留存请用群组或通过回调归档。
清理服务端历史后,本地还会有吗?
会。清理只作用于服务端存储与补发过滤,本地 SQLite 中的旧消息不会被远端操作删除,需要宿主自行清理本地库。
相关链接
- 多端在线与多设备同步:游标与补发时序
- 清除会话历史消息:服务端清理接口
- 限频、配额与容量限制:分页与帧上限
- Android IMLib · Electron IMLib:初始化与本地库说明
- 图解消息流转:离线补发在链路中的位置