本地存储与离线时长

本页说明两件事:各端 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(无痕/受限环境会自动降级为内存,刷新即丢失)。

步骤​

  1. 确认当前端的存储实现

    按下表核对 SDK 的实际存储位置;不是所有端都有本地库,不要把 Web/Flutter 当成可离线持久化。

  2. 验证落库(Android / Electron)

    Android:登录并收发若干消息后,应用私有目录下出现 nipaas_im_<appkey>.db;messages 表含 owner_id 列。

    Electron:主进程 dbPath 下出现 im-electron/<appkey>/<userId>.db;IMClient.localDb.listConversations() 能读到会话。

  3. 验证内存态(Web / Flutter)

    Web:登录后刷新页面,会话列表需要重新走 getConversationList();如果宿主没有用 IMKit.IMLocalStore 保存会话,界面上不会保留任何消息。

    Flutter:热重启(hot restart)后客户端实例重建,getHistoryMessages 只能从服务端拉取。

  4. 验证离线补发与游标

    用同一账号建连,下线期间由另一账号发送 3 条消息,再重连:应依次收到 hello(含 syncState.deliveredSeq)、3 条 message、sync_done{count:3}。

    对其中 1 条发送 ack 后断开再重连,count 应减少为 2 —— 游标已推进。

  5. 清理本地与服务端数据

    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)
Electronnode:sqlite DatabaseSync;<dbPath>/im-electron/<appkey>/<userId>.db消息、会话、草稿、去重索引、未读(localDb 26 个方法)保留(重启后可直接读本地库)localDb.clearAll/clearMessages/deleteMessagesByTime
Android系统 SQLite(SQLiteOpenHelper);库名 nipaas_im_<appkey>.dbschema v2:messages / conversations / sync_cursors / kv,按 owner_id 隔离保留(进程重启后可直接读)公开 API 仅 deleteConversation(删会话行、保留消息);无公开“清空本地库”方法
Flutter客户端实例内存(_messageCache)消息缓存、回应/扩展缓存丢失(实例重建)实例销毁;本地持久化为待补项
Native(C ABI)无仅通过 imsdk_client_poll 返回帧丢失—
Node SDK无(内存)连接态与回调丢失进程退出
iOS / HarmonyOS未实现(契约同构)无运行时代码——

离线补发相关参数​

参数值来源说明
单会话离线消息上限(条)500protocol.json limits协议声明的单会话离线补发上限;当前服务端未按此值截断
历史消息默认分页大小20protocol.json limitsgetHistoryMessages / message.history 默认分页大小
历史消息最大分页大小100protocol.json limits单页最大条数;超过 500 条未 ACK 时用该分页兜底
deliveredSeq每用户每会话一个Server.deliveredack 帧推进;hello.syncState 返回当前值
historyCleared 水位毫秒时间戳Server.history_clearedmessage.history.clean 写入;补发与历史读取同时过滤
Token 有效期7 天TOKEN_TTL_MS离线期间 Token 过期需重新签发后才能重连

帧与传输上限见 限频、配额与容量限制;补发时序见 多端在线与多设备同步。

示例​

查看连接与消息(服务端视角)​

Shell
# 连接明细中的 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'

清理服务端历史(保留最近消息)​

JavaScript
// 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 中的旧消息不会被远端操作删除,需要宿主自行清理本地库。

相关链接​