// // AiVM.m // keyBoard // // Created by Mac on 2026/1/22. // #import "AiVM.h" #import "KBAPI.h" #import "KBNetworkManager.h" #import "KBCommentModel.h" #import "KBLikedCompanionModel.h" #import "KBChattedCompanionModel.h" #import "KBChatSessionResetModel.h" #import @implementation KBAiSyncData - (void)setAudioBase64:(NSString *)audioBase64 { if (![audioBase64 isKindOfClass:[NSString class]]) { _audioBase64 = nil; self.audioData = nil; return; } _audioBase64 = [audioBase64 copy]; if (_audioBase64.length == 0) { self.audioData = nil; return; } NSString *cleanBase64 = _audioBase64; NSRange commaRange = [cleanBase64 rangeOfString:@","]; if ([cleanBase64 hasPrefix:@"data:"] && commaRange.location != NSNotFound) { cleanBase64 = [cleanBase64 substringFromIndex:commaRange.location + 1]; } self.audioData = [[NSData alloc] initWithBase64EncodedString:cleanBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]; } @end @implementation KBAiSyncResponse @end @implementation KBAiMessageData @end @implementation KBAiMessageResponse @end @implementation KBAiSpeechTranscribeData @end @implementation KBAiSpeechTranscribeResponse @end @implementation AiVM - (void)syncChatWithTranscript:(NSString *)transcript completion:(AiVMSyncCompletion)completion { if (transcript.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"transcript is empty"}]; if (completion) { completion(nil, error); } return; } NSDictionary *params = @{ @"transcript" : transcript ?: @"" }; CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); NSLog(@"[AiVM] /chat/sync request: %@", params); [[KBNetworkManager shared] POST:API_AI_CHAT_SYNC jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { CFAbsoluteTime elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0; if (error) { NSLog(@"[AiVM] /chat/sync failed: %@", error.localizedDescription ?: @""); NSLog(@"[AiVM] /chat/sync duration: %.0f ms", elapsed); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /chat/sync response received"); NSLog(@"[AiVM] /chat/sync duration: %.0f ms", elapsed); KBAiSyncResponse *model = [KBAiSyncResponse mj_objectWithKeyValues:json]; if (completion) { completion(model, nil); } }]; } - (void)requestChatMessageWithContent:(NSString *)content companionId:(NSInteger)companionId completion:(AiVMMessageCompletion)completion { if (content.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"content is empty"}]; if (completion) { completion(nil, error); } return; } NSString *encodedContent = [content stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]]; NSString *path = [NSString stringWithFormat:@"%@?content=%@&companionId=%ld", API_AI_CHAT_MESSAGE, encodedContent ?: @"", (long)companionId]; NSDictionary *params = @{ @"content" : content ?: @"", @"companionId" : @(companionId) }; [[KBNetworkManager shared] POST:path jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { KBAiMessageResponse *model = [KBAiMessageResponse mj_objectWithKeyValues:json]; if (error) { if (completion) { completion(model, error); } return; } id dataObj = json[@"data"]; if (!model.data && [dataObj isKindOfClass:[NSString class]]) { KBAiMessageData *data = [[KBAiMessageData alloc] init]; data.content = (NSString *)dataObj; model.data = data; } if (completion) { completion(model, nil); } }]; } - (void)requestAudioWithAudioId:(NSString *)audioId completion:(AiVMAudioURLCompletion)completion { if (audioId.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"audioId is empty"}]; if (completion) { completion(nil, error); } return; } NSString *path = [NSString stringWithFormat:@"/chat/audio/%@", audioId]; [[KBNetworkManager shared] GET:path parameters:nil headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { if (completion) { completion(nil, error); } return; } // 解析返回的 URL NSString *audioURL = nil; if ([json isKindOfClass:[NSDictionary class]]) { // 返回格式:{"code": 0, "data": {"audioUrl": "http://...", "url": "http://..."}} id dataObj = json[@"data"]; if ([dataObj isKindOfClass:[NSDictionary class]]) { NSDictionary *dataDict = (NSDictionary *)dataObj; // 优先使用 audioUrl,兼容 url id audioUrlObj = dataDict[@"audioUrl"] ?: dataDict[@"url"]; // 检查是否为 NSNull if (audioUrlObj && ![audioUrlObj isKindOfClass:[NSNull class]] && [audioUrlObj isKindOfClass:[NSString class]]) { audioURL = (NSString *)audioUrlObj; } } else if ([dataObj isKindOfClass:[NSString class]]) { audioURL = (NSString *)dataObj; } // 或者直接返回 URL 字符串 if (!audioURL) { id audioUrlObj = json[@"audioUrl"] ?: json[@"url"]; if (audioUrlObj && ![audioUrlObj isKindOfClass:[NSNull class]] && [audioUrlObj isKindOfClass:[NSString class]]) { audioURL = (NSString *)audioUrlObj; } } } // 如果 audioURL 为空或 nil,返回 nil(不是错误,表示音频还未生成) if (!audioURL || audioURL.length == 0) { if (completion) { completion(nil, nil); // 返回 nil 表示音频未就绪,需要重试 } return; } if (completion) { completion(audioURL, nil); } }]; } - (void)uploadAudioFileAtURL:(NSURL *)fileURL completion:(AiVMUploadAudioCompletion)completion { if (!fileURL || !fileURL.isFileURL) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"invalid fileURL"}]; if (completion) { completion(nil, error); } return; } [[KBNetworkManager shared] uploadFile:API_AI_AUDIO_UPLOAD fileURL:fileURL name:@"file" mimeType:@"audio/mp4" parameters:nil headers:nil completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { if (completion) { completion(nil, error); } return; } NSString *fileURLString = nil; id dataObj = json[@"data"]; if ([dataObj isKindOfClass:[NSString class]]) { fileURLString = (NSString *)dataObj; } else if ([dataObj isKindOfClass:[NSDictionary class]]) { id urlObj = dataObj[@"url"] ?: dataObj[@"audioUrl"]; if ([urlObj isKindOfClass:[NSString class]]) { fileURLString = (NSString *)urlObj; } } if (completion) { completion(fileURLString, nil); } }]; } - (void)transcribeAudioFileAtURL:(NSURL *)fileURL completion:(AiVMSpeechTranscribeCompletion)completion { if (!fileURL || !fileURL.isFileURL) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"invalid fileURL"}]; if (completion) { completion(nil, error); } return; } [[KBNetworkManager shared] uploadFile:API_AI_SPEECH_TRANSCRIBE fileURL:fileURL name:@"file" mimeType:@"audio/m4a" parameters:nil headers:nil completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { if (completion) { completion(nil, error); } return; } KBAiSpeechTranscribeResponse *model = [KBAiSpeechTranscribeResponse mj_objectWithKeyValues:json]; if (completion) { completion(model, nil); } }]; } #pragma mark - 人设相关接口 - (void)fetchPersonasWithPageNum:(NSInteger)pageNum pageSize:(NSInteger)pageSize completion:(void (^)(KBPersonaPageModel * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"pageNum": @(pageNum), @"pageSize": @(pageSize) }; NSLog(@"[AiVM] /ai-companion/page request: %@", params); [[KBNetworkManager shared] POST:@"/ai-companion/page" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/page failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/page response: %@", json); // 解析响应 NSInteger code = [json[@"code"] integerValue]; if (code != 0) { NSString *message = json[@"message"] ?: @"请求失败"; NSError *bizError = [NSError errorWithDomain:@"AiVM" code:code userInfo:@{NSLocalizedDescriptionKey: message}]; if (completion) { completion(nil, bizError); } return; } // 转换为模型 id dataObj = json[@"data"]; if ([dataObj isKindOfClass:[NSDictionary class]]) { KBPersonaPageModel *pageModel = [KBPersonaPageModel mj_objectWithKeyValues:dataObj]; if (completion) { completion(pageModel, nil); } } else { NSError *parseError = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}]; if (completion) { completion(nil, parseError); } } }]; } #pragma mark - 聊天记录相关接口 - (void)fetchChatHistoryWithCompanionId:(NSInteger)companionId pageNum:(NSInteger)pageNum pageSize:(NSInteger)pageSize completion:(void (^)(KBChatHistoryPageModel * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"companionId": @(companionId), @"pageNum": @(pageNum), @"pageSize": @(pageSize) }; NSLog(@"[AiVM] /chat/history request: %@", params); [[KBNetworkManager shared] POST:@"/chat/history" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /chat/history failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /chat/history response: %@", json); // 解析响应 NSInteger code = [json[@"code"] integerValue]; if (code != 0) { NSString *message = json[@"message"] ?: @"请求失败"; NSError *bizError = [NSError errorWithDomain:@"AiVM" code:code userInfo:@{NSLocalizedDescriptionKey: message}]; if (completion) { completion(nil, bizError); } return; } // 转换为模型 id dataObj = json[@"data"]; if ([dataObj isKindOfClass:[NSDictionary class]]) { KBChatHistoryPageModel *pageModel = [KBChatHistoryPageModel mj_objectWithKeyValues:dataObj]; if (completion) { completion(pageModel, nil); } } else { NSError *parseError = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}]; if (completion) { completion(nil, parseError); } } }]; } #pragma mark - 评论相关接口 - (void)addCommentWithCompanionId:(NSInteger)companionId content:(NSString *)content parentId:(nullable NSNumber *)parentId rootId:(NSInteger)rootId completion:(void (^)(NSInteger, NSError * _Nullable))completion { if (content.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}]; if (completion) { completion(-1, error); } return; } NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"companionId"] = @(companionId); params[@"content"] = content; params[@"rootId"] = @(rootId); if (parentId) { params[@"parentId"] = parentId; } NSLog(@"[AiVM] /ai-companion/comment/add request: %@", params); [[KBNetworkManager shared] POST:@"/ai-companion/comment/add" jsonBody:[params copy] headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/comment/add failed: %@", error.localizedDescription ?: @""); if (completion) { completion(-1, error); } return; } NSLog(@"[AiVM] /ai-companion/comment/add response: %@", json); NSInteger code = [json[@"code"] integerValue]; if (completion) { completion(code, nil); } }]; } - (void)fetchCommentsWithCompanionId:(NSInteger)companionId pageNum:(NSInteger)pageNum pageSize:(NSInteger)pageSize completion:(void (^)(KBCommentPageModel * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"companionId": @(companionId), @"pageNum": @(pageNum > 0 ? pageNum : 1), @"pageSize": @(pageSize > 0 ? pageSize : 20) }; NSLog(@"[AiVM] /ai-companion/comment/page request: %@", params); [[KBNetworkManager shared] POST:@"/ai-companion/comment/page" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/comment/page failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/comment/page response: %@", json); NSInteger code = [json[@"code"] integerValue]; if (code != 0) { NSString *message = json[@"message"] ?: @"请求失败"; NSError *bizError = [NSError errorWithDomain:@"AiVM" code:code userInfo:@{NSLocalizedDescriptionKey: message}]; if (completion) { completion(nil, bizError); } return; } id dataObj = json[@"data"]; if ([dataObj isKindOfClass:[NSDictionary class]]) { KBCommentPageModel *pageModel = [KBCommentPageModel mj_objectWithKeyValues:dataObj]; if (completion) { completion(pageModel, nil); } } else { NSError *parseError = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}]; if (completion) { completion(nil, parseError); } } }]; } - (void)likeCommentWithCommentId:(NSInteger)commentId completion:(void (^)(KBCommentLikeResponse * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"commentId": @(commentId) }; NSLog(@"[AiVM] /ai-companion/comment/like request: %@", params); [[KBNetworkManager shared] POST:@"/ai-companion/comment/like" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/comment/like failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/comment/like response: %@", json); KBCommentLikeResponse *likeResponse = [KBCommentLikeResponse mj_objectWithKeyValues:json]; if (completion) { completion(likeResponse, nil); } }]; } - (void)likeCompanionWithCompanionId:(NSInteger)companionId completion:(void (^)(KBCommentLikeResponse * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"companionId": @(companionId) }; NSLog(@"[AiVM] /ai-companion/like request: %@", params); [[KBNetworkManager shared] POST:@"/ai-companion/like" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/like failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/like response: %@", json); KBCommentLikeResponse *likeResponse = [KBCommentLikeResponse mj_objectWithKeyValues:json]; if (completion) { completion(likeResponse, nil); } }]; } #pragma mark - 点赞列表接口 - (void)fetchLikedCompanionsWithCompletion:(void (^)(NSArray * _Nullable, NSError * _Nullable))completion { NSLog(@"[AiVM] /ai-companion/liked request"); [[KBNetworkManager shared] GET:@"/ai-companion/liked" parameters:nil headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/liked failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/liked response: %@", json); NSInteger code = [json[@"code"] integerValue]; if (code != 0) { NSString *message = json[@"message"] ?: @"请求失败"; NSError *bizError = [NSError errorWithDomain:@"AiVM" code:code userInfo:@{NSLocalizedDescriptionKey: message}]; if (completion) { completion(nil, bizError); } return; } NSArray *dataArray = json[@"data"]; if (![dataArray isKindOfClass:[NSArray class]]) { dataArray = @[]; } NSArray *list = [KBLikedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray]; if (completion) { completion(list, nil); } }]; } - (void)fetchChattedCompanionsWithCompletion:(void (^)(NSArray * _Nullable, NSError * _Nullable))completion { NSLog(@"[AiVM] /ai-companion/chatted request"); [[KBNetworkManager shared] GET:@"/ai-companion/chatted" parameters:nil headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /ai-companion/chatted failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /ai-companion/chatted response: %@", json); NSInteger code = [json[@"code"] integerValue]; if (code != 0) { NSString *message = json[@"message"] ?: @"请求失败"; NSError *bizError = [NSError errorWithDomain:@"AiVM" code:code userInfo:@{NSLocalizedDescriptionKey: message}]; if (completion) { completion(nil, bizError); } return; } NSArray *dataArray = json[@"data"]; if (![dataArray isKindOfClass:[NSArray class]]) { dataArray = @[]; } NSArray *list = [KBChattedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray]; if (completion) { completion(list, nil); } }]; } #pragma mark - 会话管理接口 - (void)resetChatSessionWithCompanionId:(NSInteger)companionId completion:(void (^)(KBChatSessionResetResponse * _Nullable, NSError * _Nullable))completion { NSDictionary *params = @{ @"companionId": @(companionId) }; NSLog(@"[AiVM] /chat/session/reset request: %@", params); [[KBNetworkManager shared] POST:@"/chat/session/reset" jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { NSLog(@"[AiVM] /chat/session/reset failed: %@", error.localizedDescription ?: @""); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /chat/session/reset response: %@", json); KBChatSessionResetResponse *resetResponse = [KBChatSessionResetResponse mj_objectWithKeyValues:json]; if (completion) { completion(resetResponse, nil); } }]; } @end