会话形态与消息类型选型

接入 IM 前先回答两个问题:消息应该发到哪种会话形态?应该用哪种消息类型?本页给出决策表、能力边界与可直接运行的 Web SDK 片段(Android/Flutter 只列已确认存在的公开方法名)。

会话形态怎么选​

形态适用场景成员/规模离线与历史本项目实现
单聊 Private一对一私聊、客服会话2 人离线补发 + 历史漫游✅ 已实现
群聊 Group工作群、兴趣群、项目群默认上限 3000离线补发 + 历史漫游✅ 已实现
聊天室 Chatroom直播互动、广场、临时活动在线上限 5000仅在线;不落库、不补发;加入取最近 50 条✅ 已实现
超级群 SuperGroup大社区、频道化讨论上限 100000持久化 + 离线 + 多端;频道消息按 channelId 路由(rev23)🟡 服务端部分实现
系统通知 System由服务端下发的群变更、好友通过、封禁、广播等通知按用户落库、离线补发、多端✅ 已实现(仅服务端发送)
一句话决策

需要“消息不丢、离线能补、能翻历史” → 单聊/群聊/超级群;需要“很多人同时在线、消息过期即弃” → 聊天室;只想给用户发通知 → 系统通知。

消息类型怎么选​

需求content.type关键限制状态
纯文字 / @text5000 字符;@ 仅群聊✅ 已实现
图片imageurl 必填;先上传✅ 已实现
文件file上传上限 20 MB;消息体 ≤5120 字节✅ 已实现
语音voiceurl + duration 1..300 秒✅ 已实现
位置location经纬度范围校验;address ≤200 字✅ 已实现
业务卡片 / 投票 / 自定义结构customdata 任意 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

相关页面​