WuKongIM Docs

Flutter 流消息

使用独立 Flutter 连接、类型化流消息设置、自定义事件和业务后端恢复逐段回复。

编辑此页报告文档问题

使用 1.2.1 把一条回复逐段更新到同一个气泡。先完成 Flutter 快速接入,并确认集群包含在线 EVENT 投递实现。

1. 安装与明确职责

dependencies:
  flutter:
    sdk: flutter
  wukong_easy_sdk: 1.2.1

执行 flutter pub get。客户端接收普通消息和自定义事件,维护有限的气泡投影;可信业务后端鉴权用户、作者和频道,追加事件,提供历史与取消接口;Agent 后端持有模型密钥并按顺序写入增量。普通客户端只持有自身凭据,不拿机器人 Token 或 HTTP API 管理凭据。

2. 接收基消息和在线事件

将下面的辅助类加入 Flutter 应用。按快速接入构造 WuKongConfig,使用业务后端签发的凭据和设备可达的 WebSocket 地址。应用状态容器或页面拥有 StreamReader,只调用一次并等待 connect(),退出账号或销毁拥有者时调用 close()。回调由应用提供,更新自己的有界 UI 投影。

import 'package:wukong_easy_sdk/wukong_easy_sdk.dart';

class StreamReader {
  final sdk = WuKongEasySDK.createInstance();
  final WuKongConfig config;
  bool closed = false;

  StreamReader({
    required this.config,
    required String channelId,
    required void Function() onConnected,
    required void Function(Message) onBase,
    required void Function(EventNotification) onStreamEvent,
  }) {
    sdk.addEventListener<ConnectResult>(WuKongEvent.connect, (_) {
      if (!closed) onConnected();
    });
    sdk.addEventListener<Message>(WuKongEvent.message, (message) {
      if (closed || message.channelId != channelId ||
          message.channelType != WuKongChannelType.group ||
          message.setting?.stream != true ||
          (message.clientMsgNo ?? '').isEmpty) return;
      onBase(message);
    });
    sdk.addEventListener<EventNotification>(WuKongEvent.customEvent, (event) {
      final data = event.data;
      if (closed || data is! Map || data['channel_id'] != channelId ||
          data['channel_type'] != 2 || data['client_msg_no'] is! String ||
          (data['client_msg_no'] as String).isEmpty ||
          !event.type.startsWith('stream.') ||
          (data['event_key'] != 'main' && event.type != 'stream.finish')) return;
      onStreamEvent(event);
    });
  }

  Future<void> connect() async {
    if (closed) throw StateError('StreamReader is closed');
    try {
      await sdk.init(config);
      if (closed) { sdk.dispose(); return; }
      await sdk.connect().timeout(const Duration(seconds: 20));
    } catch (_) { sdk.disconnect(); rethrow; }
  }

  void close() {
    closed = true;
    sdk.clearAllEventListeners();
    sdk.disconnect();
    sdk.dispose();
  }
}

onConnected 在鉴权成功与自动重连后触发,由应用启动第 5 节的历史恢复。以 message.clientMsgNo 或 event.data['client_msg_no'] 找到同一个气泡。更新 UI 前过滤频道和事件通道;finish 使用保留的 __finish__ 通道。回调在 Dart 事件循环执行,应保持轻量。SDK 已解码一次 JSON 字符串形式的 EVENT data,不要再做 Base64 解码。

createInstance() 拥有独立连接和监听器集合。已有 getInstance() 继续适用于应用级单例。这个辅助类拥有整个 SDK 实例,因此可以清空监听并销毁;若共享连接,页面只能保存并移除自己的回调引用。

3. 创建流消息基消息

生成端必须先以作者自身身份完成鉴权。通常由 Agent 后端持有该连接;普通 Flutter 用户不以机器人身份登录。每次回复只生成并保存一次 clientMsgNo,核对重试结果时复用。

import 'package:wukong_easy_sdk/wukong_easy_sdk.dart';

Future<SendResult> sendStreamParent({
  required WuKongEasySDK producer,
  required String channelId,
  required String clientMsgNo,
}) async {
  if (clientMsgNo.trim().isEmpty) throw ArgumentError('Empty parent identity');
  final ack = await producer.send(
    channelId: channelId,
    channelType: WuKongChannelType.group,
    payload: {'type': 1, 'content': ''},
    options: SendOptions(
      clientMsgNo: clientMsgNo,
      setting: const MessageSetting(stream: true),
    ),
  );
  if (ack.reasonCode.value != 1) throw StateError('Stream parent rejected');
  return ack;
}

只有 ack.reasonCode.value == 1 后才能追加事件。SENDACK 表示基消息被接受,不能证明收件端已收到。流消息基消息必须持久化,不要在 header 中设置 noPersist 或 syncOnce。对应的接收标记是 Message.setting?.stream。原有独立 send 参数和 settings Map 保持兼容;类型化 options 中的非空字段优先。

4. 业务后端追加事件

基础消息作者为 author-42,群频道为 room-42,消息编号为 reply-42 时,由可信后端顺序调用 POST /message/event,依次提交下列三个 JSON 请求体:

{
  "channel_id": "room-42", "channel_type": 2, "from_uid": "author-42",
  "client_msg_no": "reply-42", "event_id": "reply-42:1",
  "event_type": "stream.open", "event_key": "main", "visibility": "public",
  "payload": { "kind": "text" }
}
{
  "channel_id": "room-42", "channel_type": 2, "from_uid": "author-42",
  "client_msg_no": "reply-42", "event_id": "reply-42:2",
  "event_type": "stream.delta", "event_key": "main", "visibility": "public",
  "payload": { "kind": "text", "delta": "你好🙂" }
}
{
  "channel_id": "room-42", "channel_type": 2, "from_uid": "author-42",
  "client_msg_no": "reply-42", "event_id": "reply-42:3",
  "event_type": "stream.finish", "visibility": "public",
  "payload": { "snapshot": { "kind": "text", "text": "你好🙂" } }
}

检查每次 HTTP 和应用层结果后再继续。写入前持久化事件 ID 与 Payload,重试复用原 ID;写入结果未知时先对账,不改用新 ID 盲目追加。服务端会在在线事件中补充 UTF-8 字节 text_offset,生产者不用自己填写它。stream.finish 使用保留的 __finish__ lane;接收端不能只按 event_key == "main" 过滤。

模型请求和事件追加放在后端,模型 Key 不进入 App、消息、日志或历史。模型接入、取消接口与 SSE 处理见 Agent 流式回复。

5. 投影、终态与恢复

onStreamEvent 交给应用的投影层处理,最小约定如下:

事件 / 场景应用行为
基础消息或 stream.open按 client_msg_no 找到同一个气泡;事件先到时也能创建它,不清空已存在文本
stream.delta按 Event ID 去重,再用 UTF-8 字节 text_offset 合并 payload.delta;你好🙂 占 10 字节,不是 Dart 的字符串长度
重叠增量 / 缺口跳过快照已覆盖的字节;遇到缺口等待完整快照,不直接拼到末尾
stream.snapshot / stream.finish以 payload.snapshot 的完整 kind: "text"、text 替换文本;finish 标记完成
取消 / 模型失败后端中止模型,写 stream.cancel / stream.error 携带部分文本快照,再写 stream.finish;保留取消 / 失败状态
终态后的迟到事件不重新打开回复;finish 不覆盖已取消或失败的状态

限制投影和缓冲大小,例如最多 40 条消息、每条 16,384 个 UTF-16 单元、每条 2,048 个去重 ID、恢复期间 2,048 个事件。完整原生示例的 StreamTimeline 展示字节合并、终态和有界投影。

在线事件为尽力投递,没有 RECVACK 或离线事件队列。 每次连接或重连成功后,由业务后端鉴权并读取一次 /channel/messagesync,传入 event_summary_mode: "full"。恢复期间暂存实时事件,先应用历史中的 event_meta.events 快照与状态,再按字节偏移回放缓冲增量。使用消息序号排序,保持问答顺序。在线期间不轮询历史,也不以 /message/eventsync 代替实时传输。

EVENT 的 message_seq 可能是十进制字符串,数值排序前先解析;气泡身份始终使用 client_msg_no。

6. 运行 Flutter Demo

随版本提供的原生示例需要 Flutter 3.41+ / Dart 3.11+,SDK 库保留原有最低要求。按官方 Flutter 示例启动后打开 流消息。输入可写开发集群的 HTTP API,连接后发送问题。在设置中选择模拟回复,或配置兼容模型 SSE 地址、模型名称和可选密钥。尝试取消、失败、接收端离线期间继续生成和重连恢复。

Android 模拟器通过 10.0.2.2 访问开发机,iOS 模拟器使用 127.0.0.1,真机需要可达的局域网地址。/route 返回的 WebSocket 地址也必须可达。浏览器还需要允许相应 CORS。离开可信开发网络时使用 HTTPS/WSS。

Demo 创建两个账号和一个群,并直接写入开发流事件,拥有两个独立 SDK 实例。模型密钥仅保留在页面内存;销毁页面会释放两个连接、恢复任务与模型 HTTP 客户端;切换页签会保留页面和连接。生产环境由后端持有生成任务并核对不确定写入结果。

完整操作见 Flutter 流消息指南。独立 1.2.1 验证记录区分原生 iOS 运行、Web 编译和正式包解析,不能推断全部平台、设备和模型供应商,也不替代历史四平台矩阵。

本页内容