This commit is contained in:
2025-11-12 15:31:22 +08:00
parent 1dbe04cdf9
commit c317afc0fe
3 changed files with 53 additions and 44 deletions

View File

@@ -14,6 +14,7 @@
@property (nonatomic, assign) NSInteger decodedPrefixBytes; // sseTextBuffer SSE @property (nonatomic, assign) NSInteger decodedPrefixBytes; // sseTextBuffer SSE
@property (nonatomic, assign) NSInteger deliveredCharCount; // SSE @property (nonatomic, assign) NSInteger deliveredCharCount; // SSE
@property (nonatomic, assign) BOOL hasEmitted; // 1 \t @property (nonatomic, assign) BOOL hasEmitted; // 1 \t
@property (nonatomic, assign) BOOL lastChunkEndedWithTab; // "\t" \t
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; // @property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; //
@property (nonatomic, strong) NSTimer *flushTimer; // @property (nonatomic, strong) NSTimer *flushTimer; //
@property (nonatomic, strong, nullable) NSError *finishError; // @property (nonatomic, strong, nullable) NSError *finishError; //
@@ -57,7 +58,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
_buffer = [NSMutableData data]; _buffer = [NSMutableData data];
_sseTextBuffer = [NSMutableString string]; _sseTextBuffer = [NSMutableString string];
_pendingQueue = [NSMutableArray array]; _pendingQueue = [NSMutableArray array];
_flushInterval = 0.10; _flushInterval = 0.1;
_splitLargeDeltasOnWhitespace = YES; _splitLargeDeltasOnWhitespace = YES;
} }
return self; return self;
@@ -90,6 +91,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.decodedPrefixBytes = 0; self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0; self.deliveredCharCount = 0;
self.hasEmitted = NO; self.hasEmitted = NO;
self.lastChunkEndedWithTab = NO;
[self.pendingQueue removeAllObjects]; [self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil; [self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil; self.finishError = nil;
@@ -108,6 +110,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.decodedPrefixBytes = 0; self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0; self.deliveredCharCount = 0;
self.hasEmitted = NO; self.hasEmitted = NO;
self.lastChunkEndedWithTab = NO;
[self.pendingQueue removeAllObjects]; [self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil; [self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil; self.finishError = nil;
@@ -251,26 +254,37 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
- (void)emitChunk:(NSString *)rawText { - (void)emitChunk:(NSString *)rawText {
if (rawText.length == 0) return; if (rawText.length == 0) return;
NSString *text = rawText; NSString *text = rawText;
// 1) /t -> \t
if (self.treatSlashTAsTab) { if (self.treatSlashTAsTab) {
text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"]; text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
} }
// 2) "\t"
if (!self.hasEmitted && self.trimLeadingTabOnce) { if (!self.hasEmitted && self.trimLeadingTabOnce) {
// \t if (text.length > 0 && [text characterAtIndex:0] == '\t') {
NSUInteger i = 0; NSUInteger start = 1;
while (i < text.length) { if (start < text.length && [text characterAtIndex:start] == ' ') start++;
unichar c = [text characterAtIndex:i]; text = [text substringFromIndex:start];
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; // 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); }); if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
self.hasEmitted = YES; self.hasEmitted = YES;
// \t
unichar lastc = [text characterAtIndex:text.length - 1];
self.lastChunkEndedWithTab = (lastc == '\t');
} }
#pragma mark - Queue/Flush #pragma mark - Queue/Flush

View File

@@ -312,7 +312,7 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
- (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle fallbackToMock:(BOOL)fallback { - (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle fallbackToMock:(BOOL)fallback {
[self kb_stopNetworkStreaming]; [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]; NSURL *url = [NSURL URLWithString:kKBStreamDemoURL];
if (!url) { if (fallback) [self kb_startMockStreamingWithSeed:seedTitle]; return; } 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 \tfetcher // /t->\t \tfetcher
fetcher.disableCompression = YES; fetcher.disableCompression = YES;
fetcher.acceptEventStream = NO; // SSE fetcher.acceptEventStream = NO; // SSE
fetcher.treatSlashTAsTab = NO; // \t \t Fetcher UI
fetcher.trimLeadingTabOnce = NO; fetcher.treatSlashTAsTab = YES;
fetcher.trimLeadingTabOnce = YES;
fetcher.flushInterval = 0.05; //
fetcher.onChunk = ^(NSString *chunk) { fetcher.onChunk = ^(NSString *chunk) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return; __strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self kb_appendChunkToStreamView:chunk]; [self kb_appendChunkToStreamView:chunk];
@@ -351,27 +353,11 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
#pragma mark - Helpers #pragma mark - Helpers
/// KBStreamTextView /// KBStreamTextView
/// - "/t" "\t" /// - KBStreamFetcher /t->\t \t
/// - "\t" /// - UI
/// -
- (void)kb_appendChunkToStreamView:(NSString *)chunk { - (void)kb_appendChunkToStreamView:(NSString *)chunk {
if (chunk.length == 0 || !self.streamTextView) return; if (chunk.length == 0 || !self.streamTextView) return;
NSString *text = [chunk stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"]; [self.streamTextView appendStreamText:chunk];
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.streamHasOutput = YES; self.streamHasOutput = YES;
} }

View File

@@ -62,6 +62,20 @@
[self addSubview:_scrollView]; [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 #pragma mark - Public API
// label label // label label
@@ -98,14 +112,11 @@
self.labels.lastObject.text = self.buffer; // self.labels.lastObject.text = self.buffer; //
[self layoutLabelsForCurrentWidth]; [self layoutLabelsForCurrentWidth];
// 2) // 2)
for (NSUInteger i = 1; i < parts.count; i++) { for (NSUInteger i = 1; i < parts.count; i++) {
// a) // a)
UILabel *current = self.labels.lastObject; UILabel *current = self.labels.lastObject;
if (self.shouldTrimSegments) { if (self.shouldTrimSegments) { current.text = KBTrimRight(current.text ?: @""); }
NSString *trimmed = [current.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
current.text = trimmed;
}
[self layoutLabelsForCurrentWidth]; [self layoutLabelsForCurrentWidth];
// b) // b)
@@ -133,9 +144,7 @@
#pragma mark - Layout Helpers #pragma mark - Layout Helpers
- (void)addLabelForText:(NSString *)text { - (void)addLabelForText:(NSString *)text {
if (self.shouldTrimSegments) { if (self.shouldTrimSegments) { text = KBTrimRight(text ?: @""); }
text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
// UILabel // UILabel
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero]; UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.numberOfLines = 0; label.numberOfLines = 0;
@@ -195,7 +204,7 @@
UILabel *current = self.labels.lastObject; UILabel *current = self.labels.lastObject;
if (!current) { return; } if (!current) { return; }
if (self.shouldTrimSegments) { if (self.shouldTrimSegments) {
NSString *trimmed = [current.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *trimmed = KBTrimRight(current.text ?: @"");
current.text = trimmed; current.text = trimmed;
self.buffer = trimmed; self.buffer = trimmed;
} }