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 流消息运行;它不替代历史四平台、全部设备或真实模型服务的验收。