会话形态与消息类型选型
接入 IM 前先回答两个问题:消息应该发到哪种会话形态?应该用哪种消息类型?本页给出决策表、能力边界与可直接运行的 Web SDK 片段(Android/Flutter 只列已确认存在的公开方法名)。
会话形态怎么选
| 形态 | 适用场景 | 成员/规模 | 离线与历史 | 本项目实现 |
|---|---|---|---|---|
| 单聊 Private | 一对一私聊、客服会话 | 2 人 | 离线补发 + 历史漫游 | ✅ 已实现 |
| 群聊 Group | 工作群、兴趣群、项目群 | 默认上限 3000 | 离线补发 + 历史漫游 | ✅ 已实现 |
| 聊天室 Chatroom | 直播互动、广场、临时活动 | 在线上限 5000 | 仅在线;不落库、不补发;加入取最近 50 条 | ✅ 已实现 |
| 超级群 SuperGroup | 大社区、频道化讨论 | 上限 100000 | 持久化 + 离线 + 多端;频道消息按 channelId 路由(rev23) | 🟡 服务端部分实现 |
| 系统通知 System | 由服务端下发的群变更、好友通过、封禁、广播等通知 | 按用户 | 落库、离线补发、多端 | ✅ 已实现(仅服务端发送) |
一句话决策
需要“消息不丢、离线能补、能翻历史” → 单聊/群聊/超级群;需要“很多人同时在线、消息过期即弃” → 聊天室;只想给用户发通知 → 系统通知。
消息类型怎么选
| 需求 | content.type | 关键限制 | 状态 |
|---|---|---|---|
| 纯文字 / @ | text | 5000 字符;@ 仅群聊 | ✅ 已实现 |
| 图片 | image | url 必填;先上传 | ✅ 已实现 |
| 文件 | file | 上传上限 20 MB;消息体 ≤5120 字节 | ✅ 已实现 |
| 语音 | voice | url + duration 1..300 秒 | ✅ 已实现 |
| 位置 | location | 经纬度范围校验;address ≤200 字 | ✅ 已实现 |
| 业务卡片 / 投票 / 自定义结构 | custom | data 任意 JSON;需自行渲染与降级 | ✅ 已实现 |
| 引用(融云 RC:ReferenceMsg) | —(文本可带 quote) | 不是独立 ContentType;当前版本未提供 | 🟡 部分能力 |
| GIF / 图文 / 小视频 / 名片 / 合并转发 / 流式 | — | 当前版本未实现 | ⛔ 未实现 |
不要用 custom 冒充内置类型
用 custom 承载 GIF/视频等可以“发出去”,但不会获得内置气泡、搜索、转发、推送文案与跨端一致渲染;对外文档不能写成“已支持该消息类型”。
第 1 步:连接
import { createClient } from '@ni-paas/im-web';
const client = createClient({
appkey: 'YOUR_APP_KEY',
serverUrl: 'https://im.example.com',
});
const res = await client.connect('USER_TOKEN');
if (res.code !== 0) throw new Error(res.msg);val options = IMClientOptions(appkey = "YOUR_APP_KEY", serverUrl = "https://im.example.com")
val client = IMClient.init(context, options)
client.connect("USER_TOKEN", object : ConnectCallback {
override fun onSuccess(userId: String) { /* 连接成功 */ }
override fun onError(error: IMError) { /* 处理错误 */ }
})final engine = await RCIMIWEngine.create(
'YOUR_APP_KEY',
RCIMIWEngineOptions.create(naviServer: 'https://im.example.com'),
);
final code = await engine.connect('USER_TOKEN', 10);
if (code != 0) throw Exception('connect failed: $code');第 2 步:单聊
单聊不需要创建会话,直接以对方 userId 作为 targetId。
import { ConversationType } from '@ni-paas/im-web';
const sent = await client.sendTextMessage(
ConversationType.PRIVATE,
'user2',
'你好,NI PaaS'
);
console.log(sent.data.messageId, sent.data.seq);client.sendTextMessage(
ConversationType.PRIVATE,
"user2",
"你好,NI PaaS",
null,
object : SendMessageCallback {
override fun onSuccess(message: Message) { /* 发送成功 */ }
override fun onError(error: IMError) { /* 处理错误 */ }
}
)final sent = await engine.sendTextMessage(
1,
'user2',
'你好,NI PaaS',
);第 3 步:群聊
群组需要先创建或由服务端创建,客户端拿到 groupId 后按 ConversationType.GROUP 发送。
import { ConversationType } from '@ni-paas/im-web';
const group = await client.createGroup({
groupId: 'group_1',
name: '项目群',
members: ['user2', 'user3'],
});
if (group.code !== 0) throw new Error(group.msg);
await client.sendTextMessage(ConversationType.GROUP, 'group_1', '群聊已创建');client.createGroup(
CreateGroupOptions(groupId = "group_1", name = "项目群", members = listOf("user2", "user3")),
object : ResultCallback<Group> {
override fun onSuccess(group: Group) { /* 创建成功 */ }
override fun onError(error: IMError) { /* 处理错误 */ }
}
)final group = await engine.createGroup(
groupId: 'group_1',
name: '项目群',
members: ['user2', 'user3'],
);第 4 步:聊天室
聊天室必须先加入才能收发;消息只投递给在线成员,不落库、不补发。
import { ConversationType } from '@ni-paas/im-web';
const room = await client.createChatroom({ chatroomId: 'room_1', name: '直播互动' });
if (room.code !== 0) throw new Error(room.msg);
await client.joinChatroom('room_1');
await client.sendTextMessage(ConversationType.CHATROOM, 'room_1', '欢迎来到直播间');
// 退出后不再接收该聊天室消息
await client.leaveChatroom('room_1');client.joinChatroom("room_1", object : ResultCallback<Chatroom> {
override fun onSuccess(chatroom: Chatroom) {
client.sendTextMessage(ConversationType.CHATROOM, "room_1", "欢迎来到直播间", null,
object : SendMessageCallback {
override fun onSuccess(message: Message) { }
override fun onError(error: IMError) { }
})
}
override fun onError(error: IMError) { }
})await engine.joinChatroom('room_1');
// 3 = 聊天室(ConversationType.CHATROOM)
await engine.sendTextMessage(3, 'room_1', '欢迎来到直播间');第 5 步:超级群
超级群通过服务端创建(groupType=super),客户端按超级群会话(10)发送,rev23 起可带 channelId 发频道消息。频道与用户组通过服务端 API 管理;频道级隔离由服务端按频道成员执行。
JavaScript
import { createHash, randomBytes } from 'node:crypto';
const BASE = 'https://im.example.com';
const APP_KEY = 'demo';
const APP_SECRET = 'YOUR_APP_SECRET';
const nonce = randomBytes(16).toString('hex');
const timestamp = String(Date.now());
const signature = createHash('sha1').update(APP_SECRET + nonce + timestamp).digest('hex');
const res = await fetch(BASE + '/im/v2/group/create', {
method: 'POST',
headers: {
'content-type': 'application/json',
'App-Key': APP_KEY,
Nonce: nonce,
Timestamp: timestamp,
Signature: signature,
},
body: JSON.stringify({
groupId: 'sg_1',
name: '大社区',
owner: 'user1',
members: ['user2'],
groupType: 'super',
}),
});
console.log(await res.json());JavaScript
import { ConversationType } from '@ni-paas/im-web';
// 超级群在消息层仍按 GROUP 发送;targetId 为超级群 groupId
await client.sendTextMessage(ConversationType.GROUP, 'sg_1', '欢迎加入大社区');频道与用户组接口见 超级群场景指南;创建频道不等于消息进入频道。
第 6 步:发送非文本消息
import { ConversationType } from '@ni-paas/im-web';
await client.sendImageMessage(ConversationType.PRIVATE, 'user2', {
url: 'https://cdn.example.com/photo.jpg',
width: 1280,
height: 720,
size: 245760,
});
await client.sendFileMessage(ConversationType.PRIVATE, 'user2', {
url: 'https://cdn.example.com/report.pdf',
name: 'report.pdf',
size: 245760,
mime: 'application/pdf',
});
await client.sendVoiceMessage(ConversationType.PRIVATE, 'user2', {
url: 'https://cdn.example.com/voice.aac',
duration: 8,
});
await client.sendLocationMessage(ConversationType.PRIVATE, 'user2', {
latitude: 39.908823,
longitude: 116.39747,
address: '天安门广场',
});client.sendImageMessage(ConversationType.PRIVATE, "user2",
ImageContent("https://cdn.example.com/photo.jpg", 1280, 720, 245760L), null,
object : SendMessageCallback {
override fun onSuccess(message: Message) { }
override fun onError(error: IMError) { }
})await engine.sendImageMessage(1, 'user2',
ImageContent(url: 'https://cdn.example.com/photo.jpg', width: 1280, height: 720));Android/Flutter 的内容类
片段展示的是公开方法名与调用形态;ImageContent 等参数类以各端 SDK 实际定义为准,接入前请对照对应平台的「发送消息」功能页。
第 7 步:自定义消息
JavaScript
import { ConversationType } from '@ni-paas/im-web';
await client.sendMessage({
conversationType: ConversationType.PRIVATE,
targetId: 'user2',
content: {
type: 'custom',
data: { kind: 'order', orderId: 'ord_9f2a', status: 'paid' },
},
});接收端按 content.data.kind 分发渲染;未知 kind 必须降级为“暂不支持的消息”,不能丢弃整条消息。
第 8 步:接收、历史与未读
import { ConversationType, Events } from '@ni-paas/im-web';
client.addEventListener(Events.MESSAGES, (messages) => {
for (const msg of messages) {
console.log(msg.conversationType, msg.targetId, msg.content.type);
}
});
const history = await client.getHistoryMessages(ConversationType.PRIVATE, 'user2', { limit: 20 });
await client.clearUnreadCount(ConversationType.PRIVATE, 'user2');
await client.recallMessage('MESSAGE_ID');client.getHistoryMessages(
ConversationType.PRIVATE,
"user2",
HistoryOptions(limit = 20),
object : ResultCallback<HistoryPage> {
override fun onSuccess(page: HistoryPage) { /* page.messages */ }
override fun onError(error: IMError) { }
}
)撤回限制
仅发送者或管理员可撤回;窗口 120000 毫秒(默认 2 分钟);重复撤回返回 1023。
常见选型误区
| 误区 | 后果 | 建议 |
|---|---|---|
| 用聊天室做需要历史的业务 | 重启/断线后消息不可恢复 | 改用群聊或超级群 |
| 用群聊承载万人社区 | 超过 3000 成员无法加入 | 使用 groupType=super |
| 用 custom 冒充图片/视频 | 无内置渲染、无缩略图、无搜索 | 等待内置类型,或明确标注为业务卡片 |
| 把系统通知当普通消息发 | 客户端无法发送 system 内容(1001) | 由服务端调用 message.system.publish |
| 忽略 5 KB 消息体上限 | 返回 1007,消息发送失败 | 媒体走 URL,不内联 Base64 |