This commit is contained in:
2025-12-09 14:32:21 +08:00
parent 1b2b0c1143
commit ade23e7a20

View File

@@ -21,12 +21,10 @@
#import "KBStreamTextView.h" // #import "KBStreamTextView.h" //
#import "KBStreamOverlayView.h" // #import "KBStreamOverlayView.h" //
#import "KBFunctionTagListView.h" #import "KBFunctionTagListView.h"
#import "KBStreamFetcher.h" // #import "WJXEventSource.h"
#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;
@@ -42,12 +40,12 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
@property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay; @property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay;
// //
@property (nonatomic, strong, nullable) KBStreamFetcher *streamFetcher; @property (nonatomic, strong, nullable) WJXEventSource *eventSource;
@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 eventSourceDidReceiveDone;
@property (nonatomic, assign) BOOL streamTimeoutTriggered; @property (nonatomic, copy, nullable) NSString *eventSourceSplitPrefix;
// Data // Data
//@property (nonatomic, strong) NSArray<NSString *> *itemsInternal; //@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
@@ -272,10 +270,7 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
} }
#pragma mark - Network Streaming (POST) #pragma mark - Network Streaming (WJXEventSource)
// ATS target Info.plist HTTP
//static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/talk";
- (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle { - (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle {
[self kb_stopNetworkStreaming]; [self kb_stopNetworkStreaming];
@@ -285,7 +280,6 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
NSURL *url = [NSURL URLWithString:apiUrl]; NSURL *url = [NSURL URLWithString:apiUrl];
if (!url) { return; } if (!url) { return; }
// POST Body
NSInteger characterId = 0; NSInteger characterId = 0;
if (self.loadingTagIndex != nil) { if (self.loadingTagIndex != nil) {
NSInteger idx = self.loadingTagIndex.integerValue; NSInteger idx = self.loadingTagIndex.integerValue;
@@ -294,13 +288,13 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
characterId = model.characterId; characterId = model.characterId;
} }
} }
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]; NSString *message = seedTitle.length > 0 ? seedTitle : @"aliqua non cupidatat";
NSDictionary *payload = @{ NSDictionary *payload = @{
@"characterId": @(resolvedCharacterId), @"characterId": @(resolvedCharacterId),
@"message": message.length > 0 ? message : @"aliqua non cupidatat" @"message": @"dolore ea cillum"
}; };
NSLog(@"[KBFunction] request payload: %@", payload);
NSError *bodyError = nil; NSError *bodyError = nil;
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&bodyError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&bodyError];
if (bodyError || bodyData.length == 0) { if (bodyError || bodyData.length == 0) {
@@ -308,63 +302,178 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
return; return;
} }
self.streamHasOutput = NO; // NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60];
__weak typeof(self) weakSelf = self; request.HTTPMethod = @"POST";
KBStreamFetcher *fetcher = [KBStreamFetcher fetcherWithURL:url]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
fetcher.httpMethod = @"POST";
fetcher.httpBody = bodyData;
NSMutableDictionary *headers = [@{ @"Content-Type": @"application/json" } mutableCopy];
NSString *token = KBAuthManager.shared.current.accessToken ?: @""; NSString *token = KBAuthManager.shared.current.accessToken ?: @"";
if (token.length > 0) { if (token.length > 0) {
headers[@"auth-token"] = token; [request setValue:token forHTTPHeaderField:@"auth-token"];
} }
fetcher.extraHeaders = headers; request.HTTPBody = bodyData;
// /t->\t \tfetcher
fetcher.disableCompression = YES; self.streamHasOutput = NO;
fetcher.acceptEventStream = NO; // SSE self.eventSourceSplitPrefix = nil;
// \t \t Fetcher UI self.eventSourceDidReceiveDone = NO;
fetcher.treatSlashTAsTab = YES;
fetcher.trimLeadingTabOnce = YES; __weak typeof(self) weakSelf = self;
fetcher.flushInterval = 0.05; // WJXEventSource *source = [[WJXEventSource alloc] initWithRquest:request];
fetcher.onChunk = ^(NSString *chunk) { source.ignoreRetryAction = YES;
[source addListener:^(WJXEvent * _Nonnull event) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return; __strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self kb_appendChunkToStreamView:chunk]; [self kb_handleEventSourceMessage:event];
}; } forEvent:WJXEventNameMessage queue:NSOperationQueue.mainQueue];
fetcher.onFinish = ^(NSError * _Nullable error) { [source addListener:^(WJXEvent * _Nonnull event) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return; __strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self kb_invalidateStreamIdleTimer]; [self kb_handleEventSourceError:event.error];
if (self.streamTimeoutTriggered) { } forEvent:WJXEventNameError queue:NSOperationQueue.mainQueue];
self.streamTimeoutTriggered = NO; self.eventSource = source;
return; [self.eventSource open];
}
// loading
if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
BOOL isCancelledError = (error && error.code == NSURLErrorCancelled);
if (error && !isCancelledError) { [KBHUD showInfo:KBLocalized(@"拉取失败")]; }
if (self.streamOverlay) { [self.streamOverlay finish]; }
self.streamTimeoutTriggered = NO;
};
self.streamFetcher = fetcher;
self.streamTimeoutTriggered = NO;
[self kb_restartStreamIdleTimer];
[self.streamFetcher start];
} }
- (void)kb_stopNetworkStreaming { - (void)kb_stopNetworkStreaming {
[self.streamFetcher cancel]; [self.eventSource close];
self.streamFetcher = nil; self.eventSource = nil;
self.eventSourceSplitPrefix = nil;
self.eventSourceDidReceiveDone = NO;
self.streamHasOutput = NO; self.streamHasOutput = NO;
[self kb_invalidateStreamIdleTimer]; }
self.streamTimeoutTriggered = NO;
- (void)kb_handleEventSourceMessage:(WJXEvent *)event {
if (event.data.length == 0) { return; }
NSLog(@"[KBStream] SSE raw payload: %@", event.data);
NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) { return; }
NSError *error = nil;
NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![payload isKindOfClass:[NSDictionary class]]) { return; }
NSString *type = payload[@"type"];
if (![type isKindOfClass:[NSString class]]) { return; }
if ([type isEqualToString:@"llm_chunk"]) {
NSString *chunk = [self kb_normalizedLLMChunkString:payload[@"data"]];
if (chunk.length > 0) {
[self kb_appendChunkToStreamView:chunk];
}
return;
}
if ([type isEqualToString:@"search_result"]) {
NSString *text = [self kb_formattedSearchResultString:payload[@"data"]];
if (text.length > 0) {
[self kb_appendChunkToStreamView:text];
}
return;
}
if ([type isEqualToString:@"done"]) {
self.eventSourceDidReceiveDone = YES;
[self kb_finishEventSourceWithError:nil];
return;
}
}
- (void)kb_handleEventSourceError:(NSError * _Nullable)error {
if (self.eventSourceDidReceiveDone) { return; }
[self kb_finishEventSourceWithError:error];
}
- (void)kb_finishEventSourceWithError:(NSError * _Nullable)error {
[self.eventSource close];
self.eventSource = nil;
if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil;
self.loadingTagTitle = nil;
}
BOOL shouldShowError = (error != nil);
if (shouldShowError) {
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")];
}
if (self.streamOverlay) {
[self.streamOverlay finish];
}
self.eventSourceSplitPrefix = nil;
self.eventSourceDidReceiveDone = NO;
}
#pragma mark - Event Parsing
- (NSString *)kb_normalizedLLMChunkString:(id)dataValue {
if (![dataValue isKindOfClass:[NSString class]]) { return @""; }
NSString *text = (NSString *)dataValue;
if (self.eventSourceSplitPrefix.length > 0) {
text = [self.eventSourceSplitPrefix stringByAppendingString:text ?: @""];
self.eventSourceSplitPrefix = nil;
}
text = [text stringByReplacingOccurrencesOfString:@"\r\n\t" withString:@"\t"];
text = [text stringByReplacingOccurrencesOfString:@"\n\t" withString:@"\t"];
text = [text stringByReplacingOccurrencesOfString:@"\r\t" withString:@"\t"];
while (text.length > 0) {
unichar c0 = [text characterAtIndex:0];
if (c0 == '\n' || c0 == '\r') {
text = [text substringFromIndex:1];
continue;
}
break;
}
text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
text = [text stringByReplacingOccurrencesOfString:@"<SPLIT>" withString:@"\t"];
NSString *suffix = [self kb_pendingSplitSuffixForString:text];
if (suffix.length > 0) {
self.eventSourceSplitPrefix = suffix;
text = [text substringToIndex:text.length - suffix.length];
}
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
return text;
}
- (NSString *)kb_formattedSearchResultString:(id)dataValue {
if (![dataValue isKindOfClass:[NSArray class]]) { return @""; }
NSArray *list = (NSArray *)dataValue;
NSMutableArray<NSString *> *segments = [NSMutableArray array];
[list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *payload = nil;
if ([obj isKindOfClass:[NSDictionary class]]) {
id val = obj[@"payload"];
if ([val isKindOfClass:[NSString class]]) {
payload = (NSString *)val;
}
} else if ([obj isKindOfClass:[NSString class]]) {
payload = (NSString *)obj;
}
payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (payload.length > 0) {
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 *result = [NSMutableString stringWithFormat:@"\t%@:", title.length > 0 ? title : @"Search result"];
for (NSString *line in segments) {
[result appendFormat:@"\t%@", line];
}
return result;
}
- (NSString *)kb_pendingSplitSuffixForString:(NSString *)text {
static NSString * const token = @"<SPLIT>";
if (text.length == 0) { return @""; }
NSUInteger tokenLen = token.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 = [token substringToIndex:len];
if ([suffix isEqualToString:prefix]) {
return suffix;
}
}
return @"";
} }
#pragma mark - Helpers #pragma mark - Helpers
/// KBStreamTextView /// KBStreamTextView
/// - KBStreamFetcher /t->\t \t /// - `<SPLIT>` `\t`
/// - UI /// - UI
- (void)kb_appendChunkToStreamView:(NSString *)chunk { - (void)kb_appendChunkToStreamView:(NSString *)chunk {
if (chunk.length == 0) return; if (chunk.length == 0) return;
@@ -379,42 +488,6 @@ static NSTimeInterval const kKBStreamIdleTimeout = 5.0;
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")];
} }
/// ///