处理聊天数据一致性问题

This commit is contained in:
mh 2025-12-01 18:22:33 +08:00
parent bd8703656f
commit 9405f4e42c
4 changed files with 51 additions and 13 deletions

View File

@ -1,5 +1,4 @@
// // // PhoneCallViewModel.swift
// PhoneCallViewModel.swift
// //
// Created by Leon on 2025/8/27. // Created by Leon on 2025/8/27.

View File

@ -263,6 +263,9 @@ extension SessionController: UITableViewDataSource {
let cell = tableView.dequeueReusableCell(withIdentifier: StreamChatBubbleCell.reuseIdentifier, for: indexPath) as! StreamChatBubbleCell let cell = tableView.dequeueReusableCell(withIdentifier: StreamChatBubbleCell.reuseIdentifier, for: indexPath) as! StreamChatBubbleCell
let message = streamMessages[indexPath.row] let message = streamMessages[indexPath.row]
cell.configure(message: message) cell.configure(message: message)
cell.onLongPress = { [weak self] in
self?.copyStreamMessage(at: indexPath.row)
}
return cell return cell
} }
let model = self.util.cellModels[indexPath.row] let model = self.util.cellModels[indexPath.row]
@ -338,6 +341,15 @@ extension SessionController: UITableViewDelegate {
header.ignoredScrollViewContentInsetTop = tableView.contentInset.top 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: "复制成功")
}
} }

View File

@ -84,6 +84,7 @@ class SessionController: CLBaseViewController {
private var typingCharactersPerSecond: CGFloat = 20 private var typingCharactersPerSecond: CGFloat = 20
private var lastDisplayLinkTimestamp: CFTimeInterval = 0 private var lastDisplayLinkTimestamp: CFTimeInterval = 0
private var shouldFinishTypingAfterTarget = false private var shouldFinishTypingAfterTarget = false
private var rawStreamingBuffer: String = ""
convenience init(accountID: String) { convenience init(accountID: String) {
self.init() self.init()
@ -588,6 +589,7 @@ extension SessionController {
private struct StreamChunkResult { private struct StreamChunkResult {
let text: String? let text: String?
let finished: Bool let finished: Bool
let isRawString: Bool
} }
private func processStreamChatPayload(_ payload: Any?, event: String?) { private func processStreamChatPayload(_ payload: Any?, event: String?) {
@ -595,7 +597,7 @@ extension SessionController {
let result = parseStreamChunk(payload) let result = parseStreamChunk(payload)
if let text = result.text, text.isEmpty == false { if let text = result.text, text.isEmpty == false {
appendOrUpdateIncomingChunk(text) appendOrUpdateIncomingChunk(text, isRawString: result.isRawString)
} }
if result.finished || (event?.lowercased().contains("done") ?? false) { 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 DispatchQueue.main.async { [weak self] in
guard let self = self, chunk.isEmpty == false else { return } guard let self = self, chunk.isEmpty == false else { return }
@ -612,7 +614,13 @@ extension SessionController {
if let currentId = self.currentStreamingMessageId, if let currentId = self.currentStreamingMessageId,
let index = self.streamMessages.firstIndex(where: { $0.id == currentId }) { let index = self.streamMessages.firstIndex(where: { $0.id == currentId }) {
self.typingTargetText = self.mergeText(current: self.typingTargetText, incoming: chunk) 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 messageIndex = index
} else { } else {
let message = StreamChatMessageModel(id: UUID().uuidString, let message = StreamChatMessageModel(id: UUID().uuidString,
@ -621,7 +629,13 @@ extension SessionController {
isStreaming: true) isStreaming: true)
self.streamMessages.append(message) self.streamMessages.append(message)
self.currentStreamingMessageId = message.id self.currentStreamingMessageId = message.id
self.typingTargetText = chunk if isRawString {
self.rawStreamingBuffer = chunk
self.typingTargetText = self.rawStreamingBuffer
} else {
self.rawStreamingBuffer = chunk
self.typingTargetText = chunk
}
self.typingDisplayedLength = 0 self.typingDisplayedLength = 0
self.shouldFinishTypingAfterTarget = false self.shouldFinishTypingAfterTarget = false
messageIndex = self.streamMessages.count - 1 messageIndex = self.streamMessages.count - 1
@ -689,17 +703,17 @@ extension SessionController {
if let dict = payload as? [String: Any] { if let dict = payload as? [String: Any] {
let finished = (dict["finished"] as? Bool) ?? (dict["done"] as? Bool) ?? false let finished = (dict["finished"] as? Bool) ?? (dict["done"] as? Bool) ?? false
if let text = extractText(from: dict) { 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: []), if let data = try? JSONSerialization.data(withJSONObject: dict, options: []),
let jsonString = String(data: data, encoding: .utf8) { 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 { } else if let str = payload as? String {
let normalized = str.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = str.trimmingCharacters(in: .whitespacesAndNewlines)
if normalized.isEmpty { if normalized.isEmpty {
return StreamChunkResult(text: nil, finished: false) return StreamChunkResult(text: nil, finished: false, isRawString: true)
} }
if let data = normalized.data(using: .utf8), if let data = normalized.data(using: .utf8),
@ -708,13 +722,13 @@ extension SessionController {
} }
if normalized.lowercased() == "[done]" { 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>") 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() { func resetStreamingStateIfNeeded() {
@ -831,6 +845,7 @@ extension SessionController {
typingDisplayedLength = 0 typingDisplayedLength = 0
shouldFinishTypingAfterTarget = false shouldFinishTypingAfterTarget = false
currentStreamingMessageId = nil currentStreamingMessageId = nil
rawStreamingBuffer = ""
} }
/// SSE /// SSE

View File

@ -6,6 +6,12 @@ final class StreamChatBubbleCell: UITableViewCell {
private let bubbleView = UIView() private let bubbleView = UIView()
private let messageLabel = UILabel() 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?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -32,6 +38,7 @@ final class StreamChatBubbleCell: UITableViewCell {
bubbleView.layer.masksToBounds = true bubbleView.layer.masksToBounds = true
bubbleView.setContentHuggingPriority(.defaultLow, for: .horizontal) bubbleView.setContentHuggingPriority(.defaultLow, for: .horizontal)
bubbleView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) bubbleView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
contentView.addGestureRecognizer(longPressGesture)
bubbleView.addSubview(messageLabel) bubbleView.addSubview(messageLabel)
messageLabel.numberOfLines = 0 messageLabel.numberOfLines = 0
@ -80,4 +87,9 @@ final class StreamChatBubbleCell: UITableViewCell {
layoutIfNeeded() layoutIfNeeded()
} }
} }
@objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .began else { return }
onLongPress?()
}
} }