2
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
@property (nonatomic, assign) NSInteger decodedPrefixBytes; // 已解码并写入 sseTextBuffer 的字节数(SSE)
|
||||
@property (nonatomic, assign) NSInteger deliveredCharCount; // 已回传的字符数(非 SSE,用于做增量)
|
||||
@property (nonatomic, assign) BOOL hasEmitted; // 是否已经输出过正文(用于“首段删 1 个 \t”)
|
||||
@property (nonatomic, assign) BOOL lastChunkEndedWithTab; // 上一个已输出分片是否以 "\t" 结尾(用于跨分片去除“\t 后空格”)
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; // 待回调的分片(节流输出)
|
||||
@property (nonatomic, strong) NSTimer *flushTimer; // 定时从队列取出一条回调
|
||||
@property (nonatomic, strong, nullable) NSError *finishError; // 结束时的错误(需要等队列清空再回调)
|
||||
@@ -57,7 +58,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
_buffer = [NSMutableData data];
|
||||
_sseTextBuffer = [NSMutableString string];
|
||||
_pendingQueue = [NSMutableArray array];
|
||||
_flushInterval = 0.10;
|
||||
_flushInterval = 0.1;
|
||||
_splitLargeDeltasOnWhitespace = YES;
|
||||
}
|
||||
return self;
|
||||
@@ -90,6 +91,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
self.decodedPrefixBytes = 0;
|
||||
self.deliveredCharCount = 0;
|
||||
self.hasEmitted = NO;
|
||||
self.lastChunkEndedWithTab = NO;
|
||||
[self.pendingQueue removeAllObjects];
|
||||
[self.flushTimer invalidate]; self.flushTimer = nil;
|
||||
self.finishError = nil;
|
||||
@@ -108,6 +110,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
self.decodedPrefixBytes = 0;
|
||||
self.deliveredCharCount = 0;
|
||||
self.hasEmitted = NO;
|
||||
self.lastChunkEndedWithTab = NO;
|
||||
[self.pendingQueue removeAllObjects];
|
||||
[self.flushTimer invalidate]; self.flushTimer = nil;
|
||||
self.finishError = nil;
|
||||
@@ -251,26 +254,37 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
- (void)emitChunk:(NSString *)rawText {
|
||||
if (rawText.length == 0) return;
|
||||
NSString *text = rawText;
|
||||
// 1) 统一处理 “/t” -> “\t”
|
||||
if (self.treatSlashTAsTab) {
|
||||
text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
|
||||
}
|
||||
// 2) 仅在整体首段:去掉一个起始的 "\t",以及其后紧邻的一个空格(若存在)
|
||||
if (!self.hasEmitted && self.trimLeadingTabOnce) {
|
||||
// 跳过前导空白,只删除“一个”起始的 \t
|
||||
NSUInteger i = 0;
|
||||
while (i < text.length) {
|
||||
unichar c = [text characterAtIndex:i];
|
||||
if (c == ' ' || c == '\r' || c == '\n') { i++; continue; }
|
||||
break;
|
||||
}
|
||||
if (i < text.length && [text characterAtIndex:i] == '\t') {
|
||||
NSMutableString *m = [text mutableCopy];
|
||||
[m deleteCharactersInRange:NSMakeRange(i, 1)];
|
||||
text = m;
|
||||
if (text.length > 0 && [text characterAtIndex:0] == '\t') {
|
||||
NSUInteger start = 1;
|
||||
if (start < text.length && [text characterAtIndex:start] == ' ') start++;
|
||||
text = [text substringFromIndex:start];
|
||||
}
|
||||
}
|
||||
if (text.length == 0) return;
|
||||
// 3) 从第二段开始:去掉每个段首的一个空格(即 “\t ” -> “\t”),跨分片也处理
|
||||
if (text.length > 0) {
|
||||
// 跨分片:若上个分片以 \t 结尾,本分片起始的一个或多个空格去掉一个
|
||||
if (self.lastChunkEndedWithTab) {
|
||||
NSUInteger j = 0;
|
||||
while (j < text.length && [text characterAtIndex:j] == ' ') { j++; }
|
||||
if (j > 0) {
|
||||
text = [text substringFromIndex:1]; // 仅去一个空格
|
||||
}
|
||||
}
|
||||
// 同一分片内:将 “\t ” 规范化为 “\t”(仅去一个空格)
|
||||
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
|
||||
}
|
||||
if (text.length == 0) { self.lastChunkEndedWithTab = NO; return; }
|
||||
if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
|
||||
self.hasEmitted = YES;
|
||||
// 记录末尾是否为分段分隔符 \t(用于跨分片处理)
|
||||
unichar lastc = [text characterAtIndex:text.length - 1];
|
||||
self.lastChunkEndedWithTab = (lastc == '\t');
|
||||
}
|
||||
|
||||
#pragma mark - Queue/Flush
|
||||
|
||||
@@ -312,7 +312,7 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
|
||||
|
||||
- (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle fallbackToMock:(BOOL)fallback {
|
||||
[self kb_stopNetworkStreaming];
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) { if (fallback) [self kb_startMockStreamingWithSeed:seedTitle]; return; }
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) { if (fallback) /*[self kb_startMockStreamingWithSeed:seedTitle]*/; return; }
|
||||
|
||||
NSURL *url = [NSURL URLWithString:kKBStreamDemoURL];
|
||||
if (!url) { if (fallback) [self kb_startMockStreamingWithSeed:seedTitle]; return; }
|
||||
@@ -323,8 +323,10 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
|
||||
// 由本类统一做 /t->\t 与首段去 \t,fetcher 只负责增量与协议解析
|
||||
fetcher.disableCompression = YES;
|
||||
fetcher.acceptEventStream = NO; // 响应头若是 SSE 仍会自动解析
|
||||
fetcher.treatSlashTAsTab = NO;
|
||||
fetcher.trimLeadingTabOnce = NO;
|
||||
// 将 \t 与首段去 \t 的处理下沉到 Fetcher,避免 UI 抖动
|
||||
fetcher.treatSlashTAsTab = YES;
|
||||
fetcher.trimLeadingTabOnce = YES;
|
||||
fetcher.flushInterval = 0.05; // 更接近“立刻显示”的节奏
|
||||
fetcher.onChunk = ^(NSString *chunk) {
|
||||
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
|
||||
[self kb_appendChunkToStreamView:chunk];
|
||||
@@ -351,27 +353,11 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
|
||||
#pragma mark - Helpers
|
||||
|
||||
/// 统一处理需要输出到 KBStreamTextView 的分片:
|
||||
/// - 将 "/t" 转为真正的制表符 "\t";
|
||||
/// - 若这是首段输出且文本起始(允许有前导空白)紧跟一个 "\t",只删除这一个;
|
||||
/// - 非空才追加到视图,并标记已输出。
|
||||
/// - 目前网络层(KBStreamFetcher)已做 “/t->\t、首段去一个 \t、段间去一个空格”
|
||||
/// - 这里仅负责附加到视图与标记首段状态,避免 UI 抖动
|
||||
- (void)kb_appendChunkToStreamView:(NSString *)chunk {
|
||||
if (chunk.length == 0 || !self.streamTextView) return;
|
||||
NSString *text = [chunk stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
|
||||
if (!self.streamHasOutput) {
|
||||
NSUInteger i = 0; // 跳过前导空白
|
||||
while (i < text.length) {
|
||||
unichar c = [text characterAtIndex:i];
|
||||
if (c == ' ' || c == '\r' || c == '\n') { i++; continue; }
|
||||
break;
|
||||
}
|
||||
if (i < text.length && [text characterAtIndex:i] == '\t') {
|
||||
NSMutableString *m = [text mutableCopy];
|
||||
[m deleteCharactersInRange:NSMakeRange(i, 1)];
|
||||
text = m;
|
||||
}
|
||||
}
|
||||
if (text.length == 0) return;
|
||||
[self.streamTextView appendStreamText:text];
|
||||
[self.streamTextView appendStreamText:chunk];
|
||||
self.streamHasOutput = YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,20 @@
|
||||
[self addSubview:_scrollView];
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
// 仅去掉末尾空白/换行(保留前导空白,避免“段落在完成时向左跳动”)
|
||||
static inline NSString *KBTrimRight(NSString *s) {
|
||||
if (s.length == 0) return s;
|
||||
NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
NSInteger end = (NSInteger)s.length - 1;
|
||||
while (end >= 0 && [set characterIsMember:[s characterAtIndex:(NSUInteger)end]]) {
|
||||
end--;
|
||||
}
|
||||
if (end < 0) return @"";
|
||||
return [s substringToIndex:(NSUInteger)end + 1];
|
||||
}
|
||||
|
||||
#pragma mark - Public API
|
||||
|
||||
// 边输边见:实时更新当前 label 文本,遇到分隔符就新建下一个 label
|
||||
@@ -98,14 +112,11 @@
|
||||
self.labels.lastObject.text = self.buffer; // 不裁剪,保留实时输入
|
||||
[self layoutLabelsForCurrentWidth];
|
||||
|
||||
// 2) 处理每一个分隔符:完成当前段(可选裁剪)并新建空标签,然后填入下一段的内容
|
||||
// 2) 处理每一个分隔符:完成当前段(仅裁剪“末尾空白”)并新建空标签,然后填入下一段的内容
|
||||
for (NSUInteger i = 1; i < parts.count; i++) {
|
||||
// a) 完成当前段:对外观进行最终裁剪(若开启)
|
||||
UILabel *current = self.labels.lastObject;
|
||||
if (self.shouldTrimSegments) {
|
||||
NSString *trimmed = [current.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
current.text = trimmed;
|
||||
}
|
||||
if (self.shouldTrimSegments) { current.text = KBTrimRight(current.text ?: @""); }
|
||||
[self layoutLabelsForCurrentWidth];
|
||||
|
||||
// b) 新建一个空标签,代表下一个段(即刻创建,哪怕下一段当前为空)
|
||||
@@ -133,9 +144,7 @@
|
||||
#pragma mark - Layout Helpers
|
||||
|
||||
- (void)addLabelForText:(NSString *)text {
|
||||
if (self.shouldTrimSegments) {
|
||||
text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
}
|
||||
if (self.shouldTrimSegments) { text = KBTrimRight(text ?: @""); }
|
||||
// 创建一个可换行、可点击的 UILabel
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label.numberOfLines = 0;
|
||||
@@ -195,7 +204,7 @@
|
||||
UILabel *current = self.labels.lastObject;
|
||||
if (!current) { return; }
|
||||
if (self.shouldTrimSegments) {
|
||||
NSString *trimmed = [current.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
NSString *trimmed = KBTrimRight(current.text ?: @"");
|
||||
current.text = trimmed;
|
||||
self.buffer = trimmed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user