WuKongIM Docs

iOS 流消息

使用原生 iOS SDK 接收实时流事件,创建流消息,并通过业务后端完成追加、终态和重连恢复。

编辑此页报告文档问题

使用 1.2.0 的 SendOptions、MessageSetting 和 onCustomEvent,让一条回复在同一个气泡内逐段更新。先完成 iOS 快速接入,并确认集群包含在线 EVENT 投递实现。

1. 安装与职责

.package(
    url: "https://github.com/WuKongIM/WuKongEasySDK-iOS.git",
    exact: "1.2.0"
)
位置职责
iOS 客户端用当前用户的连接材料登录;接收基础消息与实时事件;维护气泡投影
可信业务后端校验用户、作者和频道权限;追加事件;提供历史与取消接口
Agent 后端持有模型 Key,读取模型 SSE,顺序写入增量与终态

以下示例使用业务后端已授权的群频道(channel_type = 2)。服务端和 SDK 不替应用创建 UI 投影。普通终端不获得机器人 Token、模型 Key 或 WuKongIM HTTP API 管理凭据。

2. 接收基础消息与实时事件

将下面的辅助类放到 iOS 15+ 应用中。config 使用快速接入中的 WuKongConfig 创建,身份、Token 和设备可达的 WebSocket 地址来自业务后端。由应用连接管理器或 SwiftUI @StateObject 的所有者持有 StreamReader;在 Task 中调用 connect(),退出账号或释放所有者时调用 close()。连接和请求超时沿用 config,例如各 15 秒。

import Foundation
import WuKongEasySDK

@MainActor
final class StreamReader {
    private let sdk: WuKongEasySDK
    private var listeners: [EventListener] = []

    init(
        config: WuKongConfig,
        channelId: String,
        onConnected: @escaping () -> Void,
        onBase: @escaping (Message) -> Void,
        onStreamEvent: @escaping (EventNotification) -> Void
    ) {
        sdk = WuKongEasySDK(config: config)
        // SDK listeners are weak: retain every token for the reader's lifetime.
        listeners.append(sdk.onConnect { _ in onConnected() })
        listeners.append(sdk.onMessage { message in
            guard message.channelId == channelId, message.channelType == 2,
                  message.setting?.stream == true,
                  let key = message.clientMsgNo, !key.isEmpty else { return }
            onBase(message)
        })
        listeners.append(sdk.onCustomEvent { event in
            guard let data = event.data as? [String: Any],
                  data["channel_id"] as? String == channelId,
                  (data["channel_type"] as? NSNumber)?.intValue == 2,
                  let key = data["client_msg_no"] as? String, !key.isEmpty,
                  event.type.hasPrefix("stream."),
                  data["event_key"] as? String == "main" || event.type == "stream.finish"
            else { return }
            onStreamEvent(event)
        })
    }

    func connect() async throws {
        do { try await sdk.connect() }
        catch { sdk.disconnect(); throw error }
    }

    func close() {
        listeners.forEach { sdk.removeListener($0) }
        listeners.removeAll()
        sdk.disconnect()
    }
}

onConnected 在初次连接和自动重连认证成功时触发,由应用开始第 5 节的历史恢复流程。onBase 与 onStreamEvent 是由应用提供的 UI 回调:前者按 message.clientMsgNo 创建或找到气泡,后者按 event.data 中的 client_msg_no 合并内容。SDK 在主线程分发监听器,回调应保持轻量。event.data 中的数字通过 NSNumber 读取;JSON 字符串形式的 data 已由 SDK 解码,无需再次 Base64 解码。

WuKongEasySDK(config:) 创建独立连接。SDK 弱引用监听器,应用必须保存返回的 EventListener;清理时先移除监听器再断开。账号或配置变化时释放旧连接并创建新实例,不复用旧身份材料。

3. 创建流消息基础消息

生产者必须先完成认证连接。下面方法只发送当前生产者自己署名的基础消息;Agent 场景通常由可信后端持有作者连接,普通 iOS 用户不登录机器人身份。clientMsgNo 由业务层为一条回复生成并保存一次,重试沿用同一个值。

import Foundation
import WuKongEasySDK

func sendStreamParent(
    producer: WuKongEasySDK,
    channelId: String,
    clientMsgNo: String
) async throws -> SendResult {
    guard !clientMsgNo.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    else { throw WuKongError.invalidPayload("clientMsgNo must not be blank") }
    let ack = try await producer.send(
        channelId: channelId,
        channelType: .group,
        payload: MessagePayload(["type": 1, "content": ""]),
        options: SendOptions(
            clientMsgNo: clientMsgNo,
            setting: MessageSetting(stream: true)
        )
    )
    guard ack.reasonCode == 1 else { throw WuKongError.sendFailed("Stream parent rejected") }
    return ack
}

收到 reasonCode == 1 才能继续追加事件;SENDACK 成功表示基础消息被接受,不代表接收端已收到。stream: true 必须搭配持久化基础消息,不使用 Header(noPersist: true) 或 syncOnce: true。Message.setting?.stream 是接收侧对应标记。

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 字节,不是 Swift 的字符串长度
重叠增量 / 缺口跳过快照已覆盖的字节;遇到缺口等待完整快照,不直接拼到末尾
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 代替实时传输。

6. 运行原生演示

按官方示例的 iOS 步骤构建并运行 SwiftUI App,选择 流消息 页。

  • HTTP API 地址:iOS Simulator 访问宿主机可用 http://127.0.0.1:5001,真机使用开发机局域网地址。
  • /route 返回的 WebSocket 地址也必须设备可达。明文和示例 ATS 例外只用于开发;生产使用 HTTPS / WSS。
  • 点击“创建会话并连接”,发送问题;在右上角“演示设置”中修改模拟内容,或启用兼容模型、填写 URL、模型名称与 API Key。
  • 尝试取消、模拟失败、断开接收端后单独生成,再重连;确认气泡与历史快照一致。

Demo 使用两个独立实例,直接写入演示账号、频道和事件,仅适用于可写演示数据的开发集群。模型 Key 仅在屏幕内存中,离开流消息页会清空 Key、停止任务并断开连接。生产环境由可信后端托管生成任务和未确认终态。

完整代码与运行说明见 iOS 原生流式聊天指南。1.2.0 独立验收记录区分 SwiftPM、CocoaPods 正式包解析、代码编译和 iOS Simulator 流消息运行;它不替代历史四平台、全部设备或真实模型服务的验收。

本页内容