This commit is contained in:
2025-11-12 16:49:19 +08:00
parent fea22aecab
commit 2f4205ad1a
7 changed files with 144 additions and 18 deletions

View File

@@ -38,6 +38,9 @@ typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
/// 非 SSE 且一次性拿到大段文本时,是否按空格切词逐条回调(模拟“逐词流式”),默认 YES。
@property (nonatomic, assign) BOOL splitLargeDeltasOnWhitespace;
/// 调试日志:默认 YES。输出起止时刻、首包耗时、各分片内容截断等关键信息。
@property (nonatomic, assign) BOOL loggingEnabled;
// 回调(统一在主线程触发)
@property (nonatomic, copy, nullable) KBStreamFetcherChunkHandler onChunk;
@property (nonatomic, copy, nullable) KBStreamFetcherFinishHandler onFinish;

View File

@@ -18,6 +18,12 @@
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; //
@property (nonatomic, strong) NSTimer *flushTimer; //
@property (nonatomic, strong, nullable) NSError *finishError; //
// Metrics
@property (nonatomic, assign) CFAbsoluteTime tStart; // start()
@property (nonatomic, assign) CFAbsoluteTime tFirstByte; //
@property (nonatomic, assign) CFAbsoluteTime tFinish; // /
@property (nonatomic, assign) NSInteger emittedChunkCount; //
@end
// UTF-8
@@ -60,6 +66,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
_pendingQueue = [NSMutableArray array];
_flushInterval = 0.1;
_splitLargeDeltasOnWhitespace = YES;
_loggingEnabled = YES;
}
return self;
}
@@ -96,6 +103,18 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
[self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil;
self.tStart = CFAbsoluteTimeGetCurrent();
self.tFirstByte = 0;
self.tFinish = 0;
self.emittedChunkCount = 0;
if (self.loggingEnabled) {
NSLog(@"[KBStream] start url=%@ acceptSSE=%@ disableCompression=%@ flush=%.0fms splitWords=%@",
self.url.absoluteString,
self.acceptEventStream?@"YES":@"NO",
self.disableCompression?@"YES":@"NO",
self.flushInterval*1000.0,
self.splitLargeDeltasOnWhitespace?@"YES":@"NO");
}
self.task = [self.session dataTaskWithRequest:req];
[self.task resume];
}
@@ -150,6 +169,15 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
NSUInteger validLen = (self.textEncoding == NSUTF8StringEncoding)
? kb_validUTF8PrefixLen(self.buffer)
: self.buffer.length;
if (validLen > 0 && self.tFirstByte == 0) {
self.tFirstByte = CFAbsoluteTimeGetCurrent();
if (self.loggingEnabled) {
NSLog(@"[KBStream] first-bytes after %.0fms (encoding=%@, SSE=%@)",
(self.tFirstByte - self.tStart)*1000.0,
(self.textEncoding==NSUTF8StringEncoding?@"UTF-8":@"Other"),
self.isSSE?@"YES":@"NO");
}
}
if (validLen == 0) return; //
if (self.isSSE) {
@@ -181,12 +209,8 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
NSString *v = [ln substringFromIndex:5];
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
[payload appendString:v ?: @""];
[payload appendString:@"\n"]; // data
}
}
if (payload.length > 0 && [payload hasSuffix:@"\n"]) {
[payload deleteCharactersInRange:NSMakeRange(payload.length - 1, 1)];
}
if (payload.length > 0) { [self enqueueChunk:payload]; }
}
}
@@ -228,17 +252,28 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
NSString *v = [ln substringFromIndex:5];
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
[payload appendString:v ?: @""];
[payload appendString:@"\n"];
}
}
if (payload.length > 0 && [payload hasSuffix:@"\n"]) {
[payload deleteCharactersInRange:NSMakeRange(payload.length - 1, 1)];
}
if (payload.length > 0) {
[self emitChunk:payload];
NSString *delta = nil;
if ((NSInteger)payload.length >= self.deliveredCharCount) {
delta = [payload substringFromIndex:self.deliveredCharCount];
} else {
delta = payload;
}
self.deliveredCharCount = payload.length;
if (delta.length > 0) { [self emitChunk:delta]; }
}
}
self.tFinish = CFAbsoluteTimeGetCurrent();
if (self.loggingEnabled) {
double t0 = (self.tFirstByte>0? (self.tFirstByte - self.tStart)*1000.0 : -1);
double t1 = (self.tFirstByte>0? (self.tFinish - self.tFirstByte)*1000.0 : -1);
double tt = (self.tFinish - self.tStart)*1000.0;
NSLog(@"[KBStream] finish chunks=%ld firstByte=%.0fms after start, tail=%.0fms, total=%.0fms error=%@",
(long)self.emittedChunkCount, t0, t1, tt, error);
}
// finish
if (self.pendingQueue.count > 0) {
self.finishError = error;
@@ -280,6 +315,11 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
}
if (text.length == 0) { self.lastChunkEndedWithTab = NO; return; }
self.emittedChunkCount += 1;
if (self.loggingEnabled) {
NSLog(@"[KBStream] chunk#%ld len=%lu text=\"%@\"",
(long)self.emittedChunkCount, (unsigned long)text.length, KBPrintableSnippet(text, 160));
}
if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
self.hasEmitted = YES;
// \t
@@ -317,4 +357,15 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
}];
}
#pragma mark - Logging helpers
static NSString *KBPrintableSnippet(NSString *s, NSUInteger maxLen) {
if (!s) return @"";
NSString *x = [s stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
if (x.length > maxLen) {
x = [[x substringToIndex:maxLen] stringByAppendingString:@"…"];
}
return x;
}
@end

View File

@@ -21,7 +21,9 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setItems:(NSArray<NSString *> *)items;
/// 在指定 index 上显示/隐藏加载指示(若 cell 不可见,内部会记录状态,待出现时应用)
- (void)setLoading:(BOOL)loading atIndex:(NSInteger)index;
@end
NS_ASSUME_NONNULL_END

View File

@@ -10,6 +10,7 @@ static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
@interface KBFunctionTagListView () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionViewInternal;
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic, strong) NSMutableSet<NSNumber *> *loadingIndexes; // loadingindex
@end
@implementation KBFunctionTagListView
@@ -31,6 +32,7 @@ static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
[_collectionViewInternal.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
]];
_items = @[];
_loadingIndexes = [NSMutableSet set];
}
return self;
}
@@ -46,6 +48,8 @@ static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
KBFunctionTagCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBFunctionTagCellId2 forIndexPath:indexPath];
cell.titleLabel.text = (indexPath.item < self.items.count) ? self.items[indexPath.item] : @"";
BOOL loading = [self.loadingIndexes containsObject:@(indexPath.item)];
[cell setLoading:loading];
return cell;
}
@@ -65,4 +69,15 @@ static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
}
}
- (void)setLoading:(BOOL)loading atIndex:(NSInteger)index {
NSNumber *key = @(index);
if (loading) { [self.loadingIndexes addObject:key]; }
else { [self.loadingIndexes removeObject:key]; }
NSIndexPath *ip = [NSIndexPath indexPathForItem:index inSection:0];
if (ip && ip.item < [self.collectionViewInternal numberOfItemsInSection:0]) {
KBFunctionTagCell *cell = (KBFunctionTagCell *)[self.collectionViewInternal cellForItemAtIndexPath:ip];
if ([cell isKindOfClass:[KBFunctionTagCell class]]) { [cell setLoading:loading]; }
}
}
@end

View File

@@ -17,7 +17,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 头像/图标
@property (nonatomic, strong, readonly) UIImageView *iconView;
/// 显示/隐藏加载指示(小菊花)
- (void)setLoading:(BOOL)loading;
@end
NS_ASSUME_NONNULL_END

View File

@@ -11,6 +11,7 @@
@interface KBFunctionTagCell ()
@property (nonatomic, strong) UILabel *titleLabelInternal;
@property (nonatomic, strong) UIImageView *iconViewInternal;
@property (nonatomic, strong) UIActivityIndicatorView *loadingView;
@end
@implementation KBFunctionTagCell
@@ -34,6 +35,14 @@
make.right.equalTo(self.contentView.mas_right).offset(-10);
make.centerY.equalTo(self.contentView.mas_centerY);
}];
//
[self.contentView addSubview:self.loadingView];
[self.loadingView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contentView);
make.right.equalTo(self.contentView.mas_right).offset(-10);
make.width.height.mas_equalTo(16);
}];
}
return self;
}
@@ -64,10 +73,35 @@
return _titleLabelInternal;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleMedium; }
#else
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleGray; }
#endif
- (UIActivityIndicatorView *)loadingView {
if (!_loadingView) {
_loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:KBSpinnerStyle()];
_loadingView.hidesWhenStopped = YES;
_loadingView.color = [UIColor grayColor];
_loadingView.hidden = YES;
}
return _loadingView;
}
#pragma mark - Expose
- (UILabel *)titleLabel { return self.titleLabelInternal; }
- (UIImageView *)iconView { return self.iconViewInternal; }
@end
- (void)setLoading:(BOOL)loading {
if (loading) {
self.loadingView.hidden = NO;
[self.loadingView startAnimating];
} else {
[self.loadingView stopAnimating];
self.loadingView.hidden = YES;
}
}
@end

View File

@@ -40,6 +40,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
//
@property (nonatomic, strong, nullable) KBStreamFetcher *streamFetcher;
@property (nonatomic, assign) BOOL streamHasOutput; // \t
@property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // loadingindex
@property (nonatomic, copy, nullable) NSString *loadingTagTitle;
// Data
@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
@@ -212,8 +214,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
}];
self.streamOverlay = overlay;
// GET
[self kb_startNetworkStreamingWithSeed:title];
// UI cell start
}
- (void)kb_onTapStreamDelete {
@@ -258,8 +259,13 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
};
fetcher.onFinish = ^(NSError * _Nullable error) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
// loading
if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
if (error) { [KBHUD showInfo:@"拉取失败"]; }
[self.streamOverlay finish];
if (self.streamOverlay) { [self.streamOverlay finish]; }
};
self.streamFetcher = fetcher;
[self.streamFetcher start];
@@ -277,7 +283,16 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
/// - KBStreamFetcher /t->\t \t
/// - UI
- (void)kb_appendChunkToStreamView:(NSString *)chunk {
if (chunk.length == 0 || !self.streamOverlay) return;
if (chunk.length == 0) return;
// overlay cell
if (!self.streamOverlay) {
[self kb_showStreamTextViewIfNeededWithTitle:self.loadingTagTitle ?: @""];
if (self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
}
if (!self.streamOverlay) return;
[self.streamOverlay appendChunk:chunk];
self.streamHasOutput = YES;
}
@@ -287,10 +302,14 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
- (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title {
// + 访访
if ([[KBFullAccessManager shared] hasFullAccess]) {
[self kb_showStreamTextViewIfNeededWithTitle:title ?: @""];
// cell overlay
[self.tagListView setLoading:YES atIndex:index];
self.loadingTagIndex = @(index);
self.loadingTagTitle = title ?: @"";
[self kb_startNetworkStreamingWithSeed:self.loadingTagTitle];
return;
}
// App
// 访
[KBHUD showInfo:@"处理中…"];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;