This commit is contained in:
2025-12-08 20:46:24 +08:00
parent 2cc93e0b48
commit 0400d2020b
2 changed files with 182 additions and 3 deletions

View File

@@ -3,6 +3,7 @@
// //
#import "KBStreamFetcher.h" #import "KBStreamFetcher.h"
#import "KBLocalizationManager.h"
@interface KBStreamFetcher () <NSURLSessionDataDelegate> @interface KBStreamFetcher () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session; @property (nonatomic, strong) NSURLSession *session;
@@ -18,6 +19,7 @@
@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; //
@property (nonatomic, copy, nullable) NSString *pendingSplitTokenPrefix; // `<SPLIT>`
// Metrics // Metrics
@property (nonatomic, assign) CFAbsoluteTime tStart; // start() @property (nonatomic, assign) CFAbsoluteTime tStart; // start()
@@ -45,6 +47,8 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
return (remain >= expected) ? n : (NSUInteger)i; return (remain >= expected) ? n : (NSUInteger)i;
} }
static NSString * const kKBStreamSplitToken = @"<SPLIT>";
@implementation KBStreamFetcher @implementation KBStreamFetcher
+ (instancetype)fetcherWithURL:(NSURL *)url { + (instancetype)fetcherWithURL:(NSURL *)url {
@@ -68,6 +72,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
_flushInterval = 0.1; _flushInterval = 0.1;
_splitLargeDeltasOnWhitespace = YES; _splitLargeDeltasOnWhitespace = YES;
_loggingEnabled = YES; _loggingEnabled = YES;
_pendingSplitTokenPrefix = nil;
} }
return self; return self;
} }
@@ -107,6 +112,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
[self.pendingQueue removeAllObjects]; [self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil; [self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil; self.finishError = nil;
self.pendingSplitTokenPrefix = nil;
self.tStart = CFAbsoluteTimeGetCurrent(); self.tStart = CFAbsoluteTimeGetCurrent();
self.tFirstByte = 0; self.tFirstByte = 0;
@@ -138,6 +144,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
[self.pendingQueue removeAllObjects]; [self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil; [self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil; self.finishError = nil;
self.pendingSplitTokenPrefix = nil;
} }
#pragma mark - NSURLSessionDataDelegate #pragma mark - NSURLSessionDataDelegate
@@ -216,7 +223,17 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
[payload appendString:v ?: @""]; [payload appendString:v ?: @""];
} }
} }
if (payload.length > 0) { [self enqueueChunk:payload]; } if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
}
NSString *llmText = nil;
if ([self processLLMChunkPayload:payload output:&llmText]) {
if (llmText.length > 0) { [self enqueueChunk:llmText]; }
} else {
[self enqueueChunk:payload];
}
}
} }
} }
return; return;
@@ -260,6 +277,9 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
} }
} }
if (payload.length > 0) { if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
}
NSString *delta = nil; NSString *delta = nil;
if ((NSInteger)payload.length >= self.deliveredCharCount) { if ((NSInteger)payload.length >= self.deliveredCharCount) {
delta = [payload substringFromIndex:self.deliveredCharCount]; delta = [payload substringFromIndex:self.deliveredCharCount];
@@ -267,9 +287,21 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
delta = payload; delta = payload;
} }
self.deliveredCharCount = payload.length; self.deliveredCharCount = payload.length;
if (delta.length > 0) { [self emitChunk:delta]; } if (delta.length > 0) {
NSString *llmText = nil;
if ([self processLLMChunkPayload:delta output:&llmText]) {
if (llmText.length > 0) { [self emitChunk:llmText]; }
} else {
[self emitChunk:delta];
}
}
} }
} }
if (self.pendingSplitTokenPrefix.length > 0) {
NSString *carry = self.pendingSplitTokenPrefix;
self.pendingSplitTokenPrefix = nil;
if (carry.length > 0) { [self enqueueChunk:carry]; }
}
self.tFinish = CFAbsoluteTimeGetCurrent(); self.tFinish = CFAbsoluteTimeGetCurrent();
if (self.loggingEnabled) { if (self.loggingEnabled) {
@@ -348,6 +380,101 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.lastChunkEndedWithTab = (lastc == '\t'); self.lastChunkEndedWithTab = (lastc == '\t');
} }
- (BOOL)processLLMChunkPayload:(NSString *)payload output:(NSString * _Nullable __autoreleasing *)output {
if (output) { *output = nil; }
if (payload.length == 0) { return NO; }
NSData *jsonData = [payload dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) { return NO; }
NSError *jsonError = nil;
id obj = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonError];
if (jsonError || ![obj isKindOfClass:[NSDictionary class]]) { return NO; }
NSString *type = ((NSDictionary *)obj)[@"type"];
if (![type isKindOfClass:[NSString class]]) { return NO; }
if ([type isEqualToString:@"llm_chunk"]) {
id dataValue = ((NSDictionary *)obj)[@"data"];
if (![dataValue isKindOfClass:[NSString class]]) {
if (output) { *output = @""; }
return YES;
}
NSString *normalized = [self normalizedLLMDataString:(NSString *)dataValue];
if (output) { *output = normalized; }
return YES;
}
if ([type isEqualToString:@"search_result"]) {
NSString *searchText = [self normalizedSearchResultString:((NSDictionary *)obj)[@"data"]];
if (output) { *output = searchText ?: @""; }
return YES;
}
if ([type isEqualToString:@"done"]) {
if (output) { *output = @""; }
return YES;
}
return NO;
}
- (NSString *)normalizedLLMDataString:(NSString *)dataString {
NSString *combined = dataString ?: @"";
if (self.pendingSplitTokenPrefix.length > 0) {
combined = [self.pendingSplitTokenPrefix stringByAppendingString:combined];
self.pendingSplitTokenPrefix = nil;
}
if (combined.length == 0) { return @""; }
NSString *result = [combined stringByReplacingOccurrencesOfString:kKBStreamSplitToken withString:@"\t"];
NSString *suffix = [self pendingSplitPrefixSuffixForString:result];
if (suffix.length > 0) {
self.pendingSplitTokenPrefix = suffix;
result = [result substringToIndex:result.length - suffix.length];
}
return result;
}
- (NSString *)normalizedSearchResultString:(id)dataValue {
if (![dataValue isKindOfClass:[NSArray class]]) { return @""; }
NSArray *list = (NSArray *)dataValue;
NSMutableArray<NSString *> *segments = [NSMutableArray array];
for (NSUInteger i = 0; i < list.count; i++) {
id item = list[i];
NSString *payload = nil;
if ([item isKindOfClass:[NSDictionary class]]) {
id val = ((NSDictionary *)item)[@"payload"];
if ([val isKindOfClass:[NSString class]]) {
payload = (NSString *)val;
}
} else if ([item isKindOfClass:[NSString class]]) {
payload = (NSString *)item;
}
payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (payload.length == 0) { continue; }
NSString *line = [NSString stringWithFormat:@"%lu. %@", (unsigned long)(segments.count + 1), payload];
[segments addObject:line];
}
if (segments.count == 0) { return @""; }
NSString *title = KBLocalized(@"Search result");
NSMutableString *text = [NSMutableString string];
[text appendString:@"\t"];
[text appendFormat:@"%@:", title.length > 0 ? title : @"Search result"];
for (NSString *line in segments) {
[text appendString:@"\t"];
[text appendString:line];
}
return text;
}
- (NSString *)pendingSplitPrefixSuffixForString:(NSString *)text {
if (text.length == 0) { return @""; }
NSUInteger tokenLen = kKBStreamSplitToken.length;
if (tokenLen <= 1) { return @""; }
NSUInteger maxLen = MIN(tokenLen - 1, text.length);
for (NSUInteger len = maxLen; len > 0; len--) {
NSString *suffix = [text substringFromIndex:text.length - len];
NSString *prefix = [kKBStreamSplitToken substringToIndex:len];
if ([suffix isEqualToString:prefix]) {
return suffix;
}
}
return @"";
}
#pragma mark - Queue/Flush #pragma mark - Queue/Flush
- (void)enqueueChunk:(NSString *)s { - (void)enqueueChunk:(NSString *)s {

View File

@@ -25,6 +25,8 @@
#import "KBTagItemModel.h" #import "KBTagItemModel.h"
#import <MJExtension/MJExtension.h> #import <MJExtension/MJExtension.h>
static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate> @interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
// UI // UI
@property (nonatomic, strong) KBFunctionBarView *barViewInternal; @property (nonatomic, strong) KBFunctionBarView *barViewInternal;
@@ -44,6 +46,8 @@
@property (nonatomic, assign) BOOL streamHasOutput; // \t @property (nonatomic, assign) BOOL streamHasOutput; // \t
@property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // loadingindex @property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // loadingindex
@property (nonatomic, copy, nullable) NSString *loadingTagTitle; @property (nonatomic, copy, nullable) NSString *loadingTagTitle;
@property (nonatomic, strong, nullable) NSTimer *streamIdleTimer;
@property (nonatomic, assign) BOOL streamTimeoutTriggered;
// Data // Data
//@property (nonatomic, strong) NSArray<NSString *> *itemsInternal; //@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
@@ -292,6 +296,7 @@
} }
NSString *message = seedTitle.length > 0 ? seedTitle : @""; NSString *message = seedTitle.length > 0 ? seedTitle : @"";
NSInteger resolvedCharacterId = (characterId > 0) ? characterId : 75; NSInteger resolvedCharacterId = (characterId > 0) ? characterId : 75;
message = [NSString stringWithFormat:@"%@-%u",message,arc4random() % 10000];
NSDictionary *payload = @{ NSDictionary *payload = @{
@"characterId": @(resolvedCharacterId), @"characterId": @(resolvedCharacterId),
@"message": message.length > 0 ? message : @"aliqua non cupidatat" @"message": message.length > 0 ? message : @"aliqua non cupidatat"
@@ -327,15 +332,24 @@
}; };
fetcher.onFinish = ^(NSError * _Nullable error) { fetcher.onFinish = ^(NSError * _Nullable error) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return; __strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self kb_invalidateStreamIdleTimer];
if (self.streamTimeoutTriggered) {
self.streamTimeoutTriggered = NO;
return;
}
// loading // loading
if (!self.streamHasOutput && self.loadingTagIndex) { if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue]; [self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil; self.loadingTagIndex = nil; self.loadingTagTitle = nil;
} }
if (error) { [KBHUD showInfo:KBLocalized(@"拉取失败")]; } BOOL isCancelledError = (error && error.code == NSURLErrorCancelled);
if (error && !isCancelledError) { [KBHUD showInfo:KBLocalized(@"拉取失败")]; }
if (self.streamOverlay) { [self.streamOverlay finish]; } if (self.streamOverlay) { [self.streamOverlay finish]; }
self.streamTimeoutTriggered = NO;
}; };
self.streamFetcher = fetcher; self.streamFetcher = fetcher;
self.streamTimeoutTriggered = NO;
[self kb_restartStreamIdleTimer];
[self.streamFetcher start]; [self.streamFetcher start];
} }
@@ -343,6 +357,8 @@
[self.streamFetcher cancel]; [self.streamFetcher cancel];
self.streamFetcher = nil; self.streamFetcher = nil;
self.streamHasOutput = NO; self.streamHasOutput = NO;
[self kb_invalidateStreamIdleTimer];
self.streamTimeoutTriggered = NO;
} }
#pragma mark - Helpers #pragma mark - Helpers
@@ -363,6 +379,42 @@
if (!self.streamOverlay) return; if (!self.streamOverlay) return;
[self.streamOverlay appendChunk:chunk]; [self.streamOverlay appendChunk:chunk];
self.streamHasOutput = YES; self.streamHasOutput = YES;
[self kb_restartStreamIdleTimer];
}
- (void)kb_restartStreamIdleTimer {
[self.streamIdleTimer invalidate];
if (!self.streamFetcher) {
self.streamIdleTimer = nil;
return;
}
__weak typeof(self) weakSelf = self;
self.streamIdleTimer = [NSTimer scheduledTimerWithTimeInterval:kKBStreamIdleTimeout repeats:NO block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) self = weakSelf;
[self kb_handleStreamIdleTimeout];
}];
}
- (void)kb_invalidateStreamIdleTimer {
[self.streamIdleTimer invalidate];
self.streamIdleTimer = nil;
}
- (void)kb_handleStreamIdleTimeout {
[self kb_invalidateStreamIdleTimer];
if (!self.streamFetcher) { return; }
self.streamTimeoutTriggered = YES;
[self.streamFetcher cancel];
self.streamFetcher = nil;
if (self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil;
self.loadingTagTitle = nil;
}
if (self.streamOverlay) {
[self.streamOverlay finish];
}
[KBHUD showInfo:KBLocalized(@"Response timeout")];
} }
/// ///