处理聊天数据一致性问题
This commit is contained in:
parent
bd8703656f
commit
9405f4e42c
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
// PhoneCallViewModel.swift
|
||||
// // PhoneCallViewModel.swift
|
||||
|
||||
//
|
||||
// Created by Leon on 2025/8/27.
|
||||
|
|
|
|||
|
|
@ -263,6 +263,9 @@ extension SessionController: UITableViewDataSource {
|
|||
let cell = tableView.dequeueReusableCell(withIdentifier: StreamChatBubbleCell.reuseIdentifier, for: indexPath) as! StreamChatBubbleCell
|
||||
let message = streamMessages[indexPath.row]
|
||||
cell.configure(message: message)
|
||||
cell.onLongPress = { [weak self] in
|
||||
self?.copyStreamMessage(at: indexPath.row)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
let model = self.util.cellModels[indexPath.row]
|
||||
|
|
@ -338,6 +341,15 @@ extension SessionController: UITableViewDelegate {
|
|||
header.ignoredScrollViewContentInsetTop = tableView.contentInset.top
|
||||
}
|
||||
}
|
||||
|
||||
private func copyStreamMessage(at row: Int) {
|
||||
guard isStreamChatMode,
|
||||
streamMessages.indices.contains(row) else { return }
|
||||
let text = streamMessages[row].text
|
||||
guard text.isEmpty == false else { return }
|
||||
UIPasteboard.general.string = text
|
||||
Hud.toast(str: "复制成功")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class SessionController: CLBaseViewController {
|
|||
private var typingCharactersPerSecond: CGFloat = 20
|
||||
private var lastDisplayLinkTimestamp: CFTimeInterval = 0
|
||||
private var shouldFinishTypingAfterTarget = false
|
||||
private var rawStreamingBuffer: String = ""
|
||||
|
||||
convenience init(accountID: String) {
|
||||
self.init()
|
||||
|
|
@ -588,6 +589,7 @@ extension SessionController {
|
|||
private struct StreamChunkResult {
|
||||
let text: String?
|
||||
let finished: Bool
|
||||
let isRawString: Bool
|
||||
}
|
||||
|
||||
private func processStreamChatPayload(_ payload: Any?, event: String?) {
|
||||
|
|
@ -595,7 +597,7 @@ extension SessionController {
|
|||
let result = parseStreamChunk(payload)
|
||||
|
||||
if let text = result.text, text.isEmpty == false {
|
||||
appendOrUpdateIncomingChunk(text)
|
||||
appendOrUpdateIncomingChunk(text, isRawString: result.isRawString)
|
||||
}
|
||||
|
||||
if result.finished || (event?.lowercased().contains("done") ?? false) {
|
||||
|
|
@ -603,7 +605,7 @@ extension SessionController {
|
|||
}
|
||||
}
|
||||
|
||||
private func appendOrUpdateIncomingChunk(_ chunk: String) {
|
||||
private func appendOrUpdateIncomingChunk(_ chunk: String, isRawString: Bool) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, chunk.isEmpty == false else { return }
|
||||
|
||||
|
|
@ -612,7 +614,13 @@ extension SessionController {
|
|||
|
||||
if let currentId = self.currentStreamingMessageId,
|
||||
let index = self.streamMessages.firstIndex(where: { $0.id == currentId }) {
|
||||
if isRawString {
|
||||
self.rawStreamingBuffer += chunk
|
||||
self.typingTargetText = self.rawStreamingBuffer
|
||||
} else {
|
||||
self.typingTargetText = self.mergeText(current: self.typingTargetText, incoming: chunk)
|
||||
self.rawStreamingBuffer = self.typingTargetText
|
||||
}
|
||||
messageIndex = index
|
||||
} else {
|
||||
let message = StreamChatMessageModel(id: UUID().uuidString,
|
||||
|
|
@ -621,7 +629,13 @@ extension SessionController {
|
|||
isStreaming: true)
|
||||
self.streamMessages.append(message)
|
||||
self.currentStreamingMessageId = message.id
|
||||
if isRawString {
|
||||
self.rawStreamingBuffer = chunk
|
||||
self.typingTargetText = self.rawStreamingBuffer
|
||||
} else {
|
||||
self.rawStreamingBuffer = chunk
|
||||
self.typingTargetText = chunk
|
||||
}
|
||||
self.typingDisplayedLength = 0
|
||||
self.shouldFinishTypingAfterTarget = false
|
||||
messageIndex = self.streamMessages.count - 1
|
||||
|
|
@ -689,17 +703,17 @@ extension SessionController {
|
|||
if let dict = payload as? [String: Any] {
|
||||
let finished = (dict["finished"] as? Bool) ?? (dict["done"] as? Bool) ?? false
|
||||
if let text = extractText(from: dict) {
|
||||
return StreamChunkResult(text: text, finished: finished)
|
||||
return StreamChunkResult(text: text, finished: finished, isRawString: false)
|
||||
}
|
||||
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []),
|
||||
let jsonString = String(data: data, encoding: .utf8) {
|
||||
return StreamChunkResult(text: jsonString, finished: finished)
|
||||
return StreamChunkResult(text: jsonString, finished: finished, isRawString: false)
|
||||
}
|
||||
return StreamChunkResult(text: nil, finished: finished)
|
||||
return StreamChunkResult(text: nil, finished: finished, isRawString: false)
|
||||
} else if let str = payload as? String {
|
||||
let normalized = str.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if normalized.isEmpty {
|
||||
return StreamChunkResult(text: nil, finished: false)
|
||||
return StreamChunkResult(text: nil, finished: false, isRawString: true)
|
||||
}
|
||||
|
||||
if let data = normalized.data(using: .utf8),
|
||||
|
|
@ -708,13 +722,13 @@ extension SessionController {
|
|||
}
|
||||
|
||||
if normalized.lowercased() == "[done]" {
|
||||
return StreamChunkResult(text: nil, finished: true)
|
||||
return StreamChunkResult(text: nil, finished: true, isRawString: true)
|
||||
}
|
||||
|
||||
let containsDoneFlag = normalized.lowercased().contains("__end__") || normalized.lowercased().contains("<end>")
|
||||
return StreamChunkResult(text: str, finished: containsDoneFlag)
|
||||
return StreamChunkResult(text: str, finished: containsDoneFlag, isRawString: true)
|
||||
}
|
||||
return StreamChunkResult(text: nil, finished: false)
|
||||
return StreamChunkResult(text: nil, finished: false, isRawString: false)
|
||||
}
|
||||
|
||||
func resetStreamingStateIfNeeded() {
|
||||
|
|
@ -831,6 +845,7 @@ extension SessionController {
|
|||
typingDisplayedLength = 0
|
||||
shouldFinishTypingAfterTarget = false
|
||||
currentStreamingMessageId = nil
|
||||
rawStreamingBuffer = ""
|
||||
}
|
||||
|
||||
/// 将 SSE 片段安全拼接,尽量避免重复,但绝不丢字
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ final class StreamChatBubbleCell: UITableViewCell {
|
|||
|
||||
private let bubbleView = UIView()
|
||||
private let messageLabel = UILabel()
|
||||
var onLongPress: (() -> Void)?
|
||||
private lazy var longPressGesture: UILongPressGestureRecognizer = {
|
||||
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
|
||||
gesture.minimumPressDuration = 0.4
|
||||
return gesture
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
|
@ -32,6 +38,7 @@ final class StreamChatBubbleCell: UITableViewCell {
|
|||
bubbleView.layer.masksToBounds = true
|
||||
bubbleView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
bubbleView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
contentView.addGestureRecognizer(longPressGesture)
|
||||
|
||||
bubbleView.addSubview(messageLabel)
|
||||
messageLabel.numberOfLines = 0
|
||||
|
|
@ -80,4 +87,9 @@ final class StreamChatBubbleCell: UITableViewCell {
|
|||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard gesture.state == .began else { return }
|
||||
onLongPress?()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue