合并转发
合并转发将多条消息合并成一条消息,发送到目标会话。接收方可以根据合并消息中的摘要展示消息卡片,点击后展开查看各条消息。Flutter IMLib 提供创建、发送和接收消息的接口,消息选择、卡片和详情页面由 App 实现。
从 Flutter SDK 5.32.0 开始支持。合并转发消息类为 RCIMIWCombineV2Message,消息类型标识为 RC:CombineV2Msg。它继承自 RCIMIWMediaMessage,创建后使用 sendMediaMessage 发送。
接口
Future<RCIMIWCombineV2Message?> createCombineV2Message(
RCIMIWConversationType type,
String targetId,
String? channelId,
RCIMIWConversationType conversationType,
List<String> summaryList,
List<String> nameList,
List<RCIMIWCombineMsgInfo> msgList,
);
| 参数 | 类型 | 说明 |
|---|---|---|
type | RCIMIWConversationType | 合并消息要发送到的目标会话类型。 |
targetId | String | 合并消息要发送到的目标会话 ID。 |
channelId | String? | 超级群频道 ID;其他会话类型传 null。 |
conversationType | RCIMIWConversationType | 被转发消息的原始会话类型,用于标题展示。应与被转发消息的来源会话一致;原生消息定义目前按单聊或群聊场景使用。 |
summaryList | List<String> | 展示在合并消息摘要中的缩略内容。通常按“发送者:摘要”组织。SDK 会原样保存列表 ,不负责拼接或截断。 |
nameList | List<String> | 被转发消息的发送者名称列表。单聊通常传去重后的发送者名称,群聊通常传群名称。 |
msgList | List<RCIMIWCombineMsgInfo> | 被转发消息列表。每一项应包含原消息的元信息、有效的 objectName 和完整的创建输入 content。 |
RCIMIWCombineMsgInfo 格式
RCIMIWCombineMsgInfo 包含以下五个字段。虽然 Dart 属性允许为空,创建时仍应按原消息完整填写,特别是 timestamp 必须与原消息的 sentTime 一致。
| 字段 | 类型 | 说明 |
|---|---|---|
fromUserId | String? | 原消息发送者 ID。 |
targetId | String? | 原消息所属会话的目标 ID。 |
timestamp | int? | 原消息发送时间,Unix 时间戳,单位为毫秒。创建时填写原消息的 sentTime,不能省略。 |
objectName | String? | 原消息的消息类型标识,例如文本消息使用 RC:TxtMsg。自定义消息使用其注册的标识符。 |
content | Map? | 创建时传 Flutter 消息对象的完整序列化 Map;接收时为原生消息体 Map,两种格式不同。 |
消息类型标识
objectName 是原消息的协议类型标识,区分大小写。内置消息通常没有通用的 objectName 属性,需要按 messageType 填写。以下覆盖 Flutter 的全部消息类型枚举;枚举名省略 RCIMIWMessageType. 前缀。
| 消息类型 | Flutter 消息类 | objectName | 说明 |
|---|---|---|---|
text | RCIMIWTextMessage | RC:TxtMsg | 文本。 |
voice | RCIMIWVoiceMessage | RC:HQVCMsg | 高清语音;普通语音 RC:VcMsg 是另一种原生类型。 |
image | RCIMIWImageMessage | RC:ImgMsg | 图片。 |
file | RCIMIWFileMessage | RC:FileMsg | 文件。 |
sight | RCIMIWSightMessage | RC:SightMsg | 小视频。 |
gif | RCIMIWGIFMessage | RC:GIFMsg | GIF 图片,注意 GIF 大写。 |
location | RCIMIWLocationMessage | RC:LBSMsg | 位置。 |
reference | RCIMIWReferenceMessage | RC:ReferenceMsg | 引用消息;需保留完整的引用内容。 |
combineV2 | RCIMIWCombineV2Message | RC:CombineV2Msg | 合并转发消息;内层合并消息沿用其已有的内容格式。 |
recall | RCIMIWRecallNotificationMessage | RC:RcNtf | 撤回通知。 |
groupNotification | RCIMIWGroupNotificationMessage | RC:GrpNtf | 群通知。 |
informationNotification | RCIMIWInformationNotificationMessage | RC:InfoNtf | 信息通知。 |
command | RCIMIWCommandMessage | RC:CmdMsg | 命令;用于接收识别,当前不支持作为合并创建输入。 |
commandNotification | RCIMIWCommandNotificationMessage | RC:CmdNtf | 命令通知;用于接收识别,当前不支持作为合并创建输入。 |
stream | RCIMIWStreamMessage | RC:StreamMsg | 流式消息;用于接收识别,当前 iOS 不支持转换为合并创建输入。 |
custom | RCIMIWCustomMessage | 按 policy 选择,见下表 | Flutter 包装自定义消息。 |
nativeCustom | RCIMIWNativeCustomMessage | message.messageIdentifier | 注册原生自定义普通消息时使用的标识。 |
nativeCustomMedia | RCIMIWNativeCustomMediaMessage | message.messageIdentifier | 注册原生自定义媒体消息时使用的标识。 |
userCustom | RCIMIWUserCustomMessage 的业务子类 | message.objectName | 与 messageObjectName() 及注册的原生消息类标识一致;还需补充编码内容,见下文。 |
unknown | RCIMIWUnknownMessage | message.objectName | 保留收到的原始标识,无固定值;当前不支持作为合并创建输入。 |
RCIMIWCustomMessage 的标识由 RCIMIWCustomMessagePolicy 决定:
policy | objectName | 存储策略 |
|---|---|---|
normal | RC:IWNormalMsg | 存储并计入未读数。 |
storage | RC:IWStorageMsg | 存储,不计入未读数。 |
command | RC:IWCmdMsg | 不存储,不计入未读数。 |
status | RC:IWStatusMsg | 状态消息,不存储,不计入未读数。 |
包装自定义消息的 identifier 是消息体中的业务子类型(原生字段 msgType),不能用作 objectName。原生自定义消息的 messageIdentifier 才是协议标识,各端注册值应保持一致。
以上是标识对照,不能将所有类型都作为可转发消息使用。建议选择已发送成功、可展示的业务消息;撤回、群通知、信息通知等是否展示由 App 决定。不要把 unknown、命令、命令通知或流式消息直接加入跨 Android/iOS 的创建列表,无法转换的项目可能被跳过。其他原生类型标识见原生消息标识补充。
创建时的 content
创建合并消息时,content 必须是原始 Flutter 消息的完整 toJson() Map,不能传 JSON 字符串,也不能只传原生消息体中的正文。Flutter 底层先通过完整 Map 还原原生消息,再交给原生 SDK 编码。例如,文本消息创建输入包含 messageType、conversationType、targetId、senderUserId、sentTime 和 text 等字段:
final source = await engine.createTextMessage(
RCIMIWConversationType.private,
'conversation-id',
null,
'你好,合并转发',
);
if (source == null) {
return;
}
// 此处构造演示消息;实际转发应沿用历史消息中的发送者和发送时间。
source.senderUserId = 'user-a';
source.sentTime = DateTime.now().millisecondsSinceEpoch;
final info = RCIMIWCombineMsgInfo.create(
fromUserId: source.senderUserId,
targetId: source.targetId,
timestamp: source.sentTime,
objectName: 'RC:TxtMsg',
content: source.toJson(),
);
不同消息类型的 content 字段以对应 Flutter 消息对象的 toJson() 为准。不要把接收消息中的原生编码内容直接作为创建输入,具体差异见接收合并消息。
用户自定义消息的编码
RCIMIWUserCustomMessage 基类的 toJson() 在基础消息字段之外只加入 objectName,不会自动调用业务子类的 encode()。如果业务子类未自行补齐序列化,需要将编码结果写入完整 Map 的 content 键。引用消息中嵌套的用户自定义消息也需要同样处理:
Map<String, dynamic> toCombineContent(RCIMIWMessage message) {
final content = message.toJson();
if (message is RCIMIWUserCustomMessage) {
content['content'] = message.encode();
} else if (message is RCIMIWReferenceMessage) {
final reference = message.referenceMessage;
if (reference != null) {
content['referenceMessage'] = toCombineContent(reference);
}
} else if (message is RCIMIWRecallNotificationMessage) {
final original = message.originalMessage;
if (original != null) {
content['originalMessage'] = toCombineContent(original);
}
}
return content;
}
组装子记录时使用 content: toCombineContent(source)。这里 RCIMIWCombineMsgInfo.content 仍是完整 Map;仅用户自定义消息在 Map 内的 content 字段是 encode() 返回的字符串,不能将整个 Map 转成 JSON 字符串。
创建并发送
type 是目标会话类型,conversationType 是原始消息来源会话类型,两者可能不同。例如,将群聊消息合并转发到单聊时,前者是 private,后者是 group。
final combine = await engine.createCombineV2Message(
RCIMIWConversationType.private, // 目标会话类型
'target-user-id', // 目标会话 ID
null, // 非超级群不需要 channelId
RCIMIWConversationType.private, // 原始消息会话类型
<String>['Alice:你好,合并转发'], // 按所选消息生成摘要
<String>['Alice'], // 原会话中发送者名称去重
<RCIMIWCombineMsgInfo>[info], // 多条消息逐条组装后加入列表
);
if (combine == null) {
// 参数不合法或没有可转换的子消息。
return;
}
final listener = RCIMIWSendMediaMessageListener(
onMediaMessageSent: (code, message) {
print('合并转发发送完成: $code');
},
);
final code = await engine.sendMediaMessage(
combine,
listener: listener,
);
合并转发消息属于媒体消息,不能使用 sendMessage 发送。发送进度、保存和完成回调使用 RCIMIWSendMediaMessageListener。
媒体子消息应保留原消息有效的 remote 地址。合并发送只处理合并消息本身的内容文件,不会逐条上传图片、语音、文件等子消息的本地文件;接收方也不能访问发送方的 local 路径。
接收合并消息
在消息监听器中判断 RCIMIWMessageType.combineV2 或 RCIMIWCombineV2Message:
engine.onMessageReceived = (message, left, offline, hasPackage) {
if (message is! RCIMIWCombineV2Message) {
return;
}
if ((message.jsonMsgKey ?? '').isNotEmpty) {
print('合并消息内容需要通过 jsonMsgKey 下载');
return;
}
for (final item in message.msgList ?? <RCIMIWCombineMsgInfo>[]) {
if (item.objectName == 'RC:TxtMsg') {
// 接收侧的原生文本内容字段是 content,而不是 Flutter 的 text。
print(item.content?['content']);
}
}
};
接收侧 msgList 的内容格式
接收或创建接口返回的 msgList 中,content 是原生消息 encode() 后的消息体 Map,不是 Flutter 消息对象的完整 toJson()。文本消息的典型结构如下:
{
"fromUserId": "user-a",
"targetId": "conversation-id",
"timestamp": 1693382006578,
"objectName": "RC:TxtMsg",
"content": {
"content": "你好,合并转发",
"user": {
"id": "user-a",
"name": "Alice",
"portrait": "https://example.com/avatar.png"
},
"extra": "业务扩展"
}
}
常见原生消息体字段示例:
| 消息 | 接收侧 content 中的典型字段 |
|---|---|
| 文本 | content |
| 图片 | content、imageUri、localPath、isFull |
| 文件 | name、size、type、fileUrl、localPath |
| 通用消息信息 | user、extra、mentionedInfo 等 |
因此,接收侧的 item.content 不能直接传给 RCIMIWTextMessage.fromJson(),也不能直接传回 createCombineV2Message。例如,接收侧文本的 content['content'] 需要先转换为 Flutter 的 RCIMIWTextMessage.text;如果要再次创建合并消息,应重新构建对应的 Flutter 消息对象,再使用该对象的 toJson()。
合并转发子记录的外层只保存发送者、目标会话、发送时间、消息类型标识和消息内容,不包含原消息的 messageUId、已读状态或 quoteInfo 等外层属性。RC:ReferenceMsg 自身的消息体仍可能包含被引用消息的 UID 等字段,但不能依赖合并子记录还原原会话的完整引用关系或执行原消息的已读、撤回等操作。
大消息和 jsonMsgKey
底层会根据合并内容大小自动选择内联 msgList 或 JSON 文件。业务代码根据 jsonMsgKey 是否有值决定读取方式,无需自行判断字节阈值。
当 jsonMsgKey 非空时,Flutter wrapper 会将 msgList 置为空,避免把大内容重复传到 Dart 层。此时需要调用 downloadMediaMessage,在下载完成回调中读取 local 文件:
import 'dart:convert';
import 'dart:io';
final listener = RCIMIWDownloadMediaMessageListener(
onMediaMessageDownloaded: (code, downloaded) async {
if (code != 0 || downloaded?.local == null) {
print('合并消息下载失败: $code');
return;
}
final local = downloaded!.local!;
final filePath = local.startsWith('file://')
? Uri.parse(local).toFilePath()
: local;
final jsonText = await File(filePath).readAsString();
final rawList = jsonDecode(jsonText);
if (rawList is! List) {
return;
}
final msgList = rawList
.whereType<Map>()
.map((item) => RCIMIWCombineMsgInfo.fromJson(
Map<String, dynamic>.from(item),
))
.toList();
for (final item in msgList) {
print('${item.objectName}: ${item.content}');
}
},
);
await engine.downloadMediaMessage(
combineMessage,
listener: listener,
);
下载文件的根结构是 JSON 数组,数组每项仍然是 fromUserId、targetId、timestamp、objectName、content 五个字段。下载完成后 wrapper 只更新媒体消息的 local 路径,不会自动把文件内容重新填充到 msgList。
注意事项
msgList的每一项都应填写五个字段;content使用完整的 Flutter 序列化 Map,用户自定义消息还需要补齐encode()结果。- 外层元信息与
content内的senderUserId、targetId、sentTime应保持一致。Android 依据content重建原消息,不会自动补入外层元信息;iOS 会使用外层timestamp设置原消息发送时间,不能省略它。 summaryList和nameList是展示信息,SDK 不负责根据msgList自动生成摘要或名称。- 需要再次转发时,先把接收侧原生消息体转换为对应的 Flutter 消息对象;不要把
content: {"content": "..."}当成RCIMIWTextMessage.toJson()。 - 大消息场景下,先下载
local文件再解析;不要以msgList是否为空判断消息是否损坏。