Files
keyboard/keyBoard/Class/AiTalk/VM/LLMStreamClient.m

245 lines
6.5 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LLMStreamClient.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "LLMStreamClient.h"
@interface LLMStreamClient () <NSURLSessionDataDelegate>
@property(nonatomic, strong) NSURLSession *urlSession;
@property(nonatomic, strong) NSURLSessionDataTask *dataTask;
@property(nonatomic, strong) dispatch_queue_t networkQueue;
@property(nonatomic, assign) BOOL generating;
@property(nonatomic, strong) NSMutableString *buffer; // SSE 数据缓冲
@end
@implementation LLMStreamClient
- (instancetype)init {
self = [super init];
if (self) {
_networkQueue = dispatch_queue_create("com.keyboard.aitalk.llm.network",
DISPATCH_QUEUE_SERIAL);
_buffer = [[NSMutableString alloc] init];
// TODO: 替换为实际的 LLM 服务器地址
_serverURL = @"https://your-llm-server.com/api/chat/stream";
}
return self;
}
- (void)dealloc {
[self cancel];
}
#pragma mark - Public Methods
- (void)sendUserText:(NSString *)text
conversationId:(NSString *)conversationId {
dispatch_async(self.networkQueue, ^{
[self cancelInternal];
self.generating = YES;
[self.buffer setString:@""];
// 创建请求
NSURL *url = [NSURL URLWithString:self.serverURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"];
if (self.apiKey) {
[request setValue:[NSString stringWithFormat:@"Bearer %@", self.apiKey]
forHTTPHeaderField:@"Authorization"];
}
// 请求体
NSDictionary *body = @{
@"message" : text,
@"conversationId" : conversationId,
@"stream" : @YES
};
NSError *jsonError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:body
options:0
error:&jsonError];
if (jsonError) {
[self reportError:jsonError];
return;
}
request.HTTPBody = jsonData;
// 创建会话
NSURLSessionConfiguration *config =
[NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 60;
config.timeoutIntervalForResource = 300;
self.urlSession = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:nil];
self.dataTask = [self.urlSession dataTaskWithRequest:request];
[self.dataTask resume];
NSLog(@"[LLMStreamClient] Started request for conversation: %@",
conversationId);
});
}
- (void)cancel {
dispatch_async(self.networkQueue, ^{
[self cancelInternal];
});
}
#pragma mark - Private Methods
- (void)cancelInternal {
self.generating = NO;
if (self.dataTask) {
[self.dataTask cancel];
self.dataTask = nil;
}
if (self.urlSession) {
[self.urlSession invalidateAndCancel];
self.urlSession = nil;
}
[self.buffer setString:@""];
}
- (void)reportError:(NSError *)error {
self.generating = NO;
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.delegate respondsToSelector:@selector(llmClientDidFail:)]) {
[self.delegate llmClientDidFail:error];
}
});
}
- (void)reportComplete {
self.generating = NO;
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.delegate respondsToSelector:@selector(llmClientDidComplete)]) {
[self.delegate llmClientDidComplete];
}
});
}
- (void)reportToken:(NSString *)token {
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.delegate
respondsToSelector:@selector(llmClientDidReceiveToken:)]) {
[self.delegate llmClientDidReceiveToken:token];
}
});
}
#pragma mark - SSE Parsing
- (void)parseSSEData:(NSData *)data {
NSString *string = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
if (!string)
return;
[self.buffer appendString:string];
// SSE 格式:每个事件以 \n\n 分隔
NSArray *events = [self.buffer componentsSeparatedByString:@"\n\n"];
// 保留最后一个可能不完整的事件
if (events.count > 1) {
[self.buffer setString:events.lastObject];
for (NSUInteger i = 0; i < events.count - 1; i++) {
[self handleSSEEvent:events[i]];
}
}
}
- (void)handleSSEEvent:(NSString *)event {
if (event.length == 0)
return;
// 解析 SSE 事件
// 格式: data: {...}
NSArray *lines = [event componentsSeparatedByString:@"\n"];
for (NSString *line in lines) {
if ([line hasPrefix:@"data: "]) {
NSString *dataString = [line substringFromIndex:6];
// 检查是否是结束标志
if ([dataString isEqualToString:@"[DONE]"]) {
[self reportComplete];
return;
}
// 解析 JSON
NSData *jsonData = [dataString dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonError = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&jsonError];
if (jsonError) {
NSLog(@"[LLMStreamClient] Failed to parse SSE data: %@", dataString);
continue;
}
// 提取 token根据实际 API 格式调整)
// 常见格式: {"token": "..."} 或 {"choices": [{"delta": {"content":
// "..."}}]}
NSString *token = json[@"token"];
if (!token) {
// OpenAI 格式
NSArray *choices = json[@"choices"];
if (choices.count > 0) {
NSDictionary *delta = choices[0][@"delta"];
token = delta[@"content"];
}
}
if (token && token.length > 0) {
[self reportToken:token];
}
}
}
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[self parseSSEData:data];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
if (error.code != NSURLErrorCancelled) {
[self reportError:error];
}
} else {
// 处理缓冲区中剩余的数据
if (self.buffer.length > 0) {
[self handleSSEEvent:self.buffer];
[self.buffer setString:@""];
}
[self reportComplete];
}
}
@end