Files
keyboard/keyBoard/Class/AiTalk/VM/AiVM.m
2026-01-28 16:35:47 +08:00

695 lines
24 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.

//
// 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 <MJExtension/MJExtension.h>
@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<KBLikedCompanionModel *> * _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<KBLikedCompanionModel *> *list = [KBLikedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray];
if (completion) {
completion(list, nil);
}
}];
}
- (void)fetchChattedCompanionsWithCompletion:(void (^)(NSArray<KBChattedCompanionModel *> * _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<KBChattedCompanionModel *> *list = [KBChattedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray];
if (completion) {
completion(list, nil);
}
}];
}
@end