Files
keyboard/CustomKeyboard/VM/KBVM.m
2026-01-30 13:26:02 +08:00

335 lines
11 KiB
Objective-C

//
// KBVM.m
// CustomKeyboard
//
#import "KBVM.h"
#import "KBNetworkManager.h"
#import "KBConfig.h"
#import <AVFoundation/AVFoundation.h>
@implementation KBChatResponse
@end
@implementation KBAudioResponse
@end
@interface KBVM ()
@property (nonatomic, strong) NSCache<NSString *, UIImage *> *avatarCache;
@end
@implementation KBVM
+ (instancetype)shared {
static KBVM *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[KBVM alloc] init];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
_avatarCache = [[NSCache alloc] init];
_avatarCache.countLimit = 20;
}
return self;
}
#pragma mark - Chat API
- (void)sendChatMessageWithContent:(NSString *)content
companionId:(NSInteger)companionId
completion:(KBChatCompletion)completion {
if (content.length == 0) {
if (completion) {
KBChatResponse *response = [[KBChatResponse alloc] init];
response.success = NO;
response.errorMessage = @"内容为空";
completion(response);
}
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
completion:^(NSDictionary *json, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
KBChatResponse *chatResponse = [[KBChatResponse alloc] init];
if (error) {
chatResponse.success = NO;
chatResponse.errorMessage = error.localizedDescription ?: @"请求失败";
if (completion) completion(chatResponse);
return;
}
// 解析文本
chatResponse.text = [self p_parseTextFromJSON:json];
// 解析 audioId
chatResponse.audioId = [self p_parseAudioIdFromJSON:json];
chatResponse.success = (chatResponse.text.length > 0);
if (!chatResponse.success) {
chatResponse.errorMessage = @"未获取到回复内容";
}
if (completion) completion(chatResponse);
});
}];
}
#pragma mark - Audio API
- (void)fetchAudioURLWithAudioId:(NSString *)audioId
completion:(KBAudioURLCompletion)completion {
if (audioId.length == 0) {
if (completion) {
KBAudioResponse *response = [[KBAudioResponse alloc] init];
response.success = NO;
response.errorMessage = @"audioId 为空";
completion(response);
}
return;
}
NSString *path = [NSString stringWithFormat:@"/chat/audio/%@", audioId];
[[KBNetworkManager shared] GET:path
parameters:nil
headers:nil
completion:^(NSDictionary *json, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
KBAudioResponse *audioResponse = [[KBAudioResponse alloc] init];
if (error) {
audioResponse.success = NO;
audioResponse.errorMessage = error.localizedDescription;
if (completion) completion(audioResponse);
return;
}
// 解析 audioURL
NSString *audioURL = [self p_parseAudioURLFromJSON:json];
audioResponse.audioURL = audioURL;
audioResponse.success = (audioURL.length > 0);
if (completion) completion(audioResponse);
});
}];
}
- (void)pollAudioURLWithAudioId:(NSString *)audioId
maxRetries:(NSInteger)maxRetries
interval:(NSTimeInterval)interval
completion:(KBAudioURLCompletion)completion {
[self p_pollAudioURLWithAudioId:audioId
retryCount:0
maxRetries:maxRetries
interval:interval
completion:completion];
}
- (void)p_pollAudioURLWithAudioId:(NSString *)audioId
retryCount:(NSInteger)retryCount
maxRetries:(NSInteger)maxRetries
interval:(NSTimeInterval)interval
completion:(KBAudioURLCompletion)completion {
[self fetchAudioURLWithAudioId:audioId completion:^(KBAudioResponse *response) {
if (response.success && response.audioURL.length > 0) {
// 成功获取到 URL
if (completion) completion(response);
return;
}
// 如果还没达到最大重试次数,继续轮询
if (retryCount < maxRetries - 1) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self p_pollAudioURLWithAudioId:audioId
retryCount:retryCount + 1
maxRetries:maxRetries
interval:interval
completion:completion];
});
} else {
// 达到最大重试次数
KBAudioResponse *failResponse = [[KBAudioResponse alloc] init];
failResponse.success = NO;
failResponse.errorMessage = [NSString stringWithFormat:@"轮询失败,已重试 %ld 次", (long)maxRetries];
if (completion) completion(failResponse);
}
}];
}
- (void)downloadAudioFromURL:(NSString *)urlString
completion:(KBAudioDataCompletion)completion {
if (urlString.length == 0) {
if (completion) {
KBAudioResponse *response = [[KBAudioResponse alloc] init];
response.success = NO;
response.errorMessage = @"URL 为空";
completion(response);
}
return;
}
[[KBNetworkManager shared] GETData:urlString
parameters:nil
headers:nil
completion:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
KBAudioResponse *audioResponse = [[KBAudioResponse alloc] init];
if (error || !data || data.length == 0) {
audioResponse.success = NO;
audioResponse.errorMessage = error.localizedDescription ?: @"下载失败";
if (completion) completion(audioResponse);
return;
}
audioResponse.audioData = data;
// 计算音频时长
NSError *playerError = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:&playerError];
if (!playerError && player) {
audioResponse.duration = player.duration;
}
audioResponse.success = YES;
if (completion) completion(audioResponse);
});
}];
}
#pragma mark - Avatar API
- (void)downloadAvatarFromURL:(NSString *)urlString
completion:(KBAvatarCompletion)completion {
if (urlString.length == 0) {
if (completion) completion(nil, nil);
return;
}
// 检查缓存
UIImage *cached = [self.avatarCache objectForKey:urlString];
if (cached) {
if (completion) completion(cached, nil);
return;
}
[[KBNetworkManager shared] GETData:urlString
parameters:nil
headers:nil
completion:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error || data.length == 0) {
if (completion) completion(nil, error);
return;
}
UIImage *image = [UIImage imageWithData:data];
if (image) {
[self.avatarCache setObject:image forKey:urlString];
}
if (completion) completion(image, nil);
});
}];
}
#pragma mark - Helper
- (NSInteger)selectedCompanionIdFromAppGroup {
NSDictionary *persona = [self selectedPersonaFromAppGroup];
if (persona) {
id companionIdObj = persona[@"personaId"] ?: persona[@"companionId"] ?: persona[@"id"];
if ([companionIdObj respondsToSelector:@selector(integerValue)]) {
return [companionIdObj integerValue];
}
}
return 0;
}
- (nullable NSDictionary *)selectedPersonaFromAppGroup {
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
return [shared objectForKey:@"AppGroup_SelectedPersona"];
}
#pragma mark - Private Parse Methods
/// 解析聊天文本
- (NSString *)p_parseTextFromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) return @"";
id dataObj = json[@"data"];
if ([dataObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *data = (NSDictionary *)dataObj;
// 优先读取 aiResponse 字段
NSArray *keys = @[@"aiResponse", @"content", @"text", @"message"];
for (NSString *key in keys) {
id value = data[key];
if ([value isKindOfClass:[NSString class]] && ((NSString *)value).length > 0) {
return (NSString *)value;
}
}
} else if ([dataObj isKindOfClass:[NSString class]]) {
return (NSString *)dataObj;
}
return @"";
}
/// 解析 audioId
- (NSString *)p_parseAudioIdFromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) return nil;
id dataObj = json[@"data"];
if ([dataObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *data = (NSDictionary *)dataObj;
NSString *audioId = data[@"audioId"];
if ([audioId isKindOfClass:[NSString class]] && audioId.length > 0) {
return audioId;
}
}
// 兼容其他字段名
NSArray *keys = @[@"audioId", @"audio_id"];
for (NSString *key in keys) {
id value = json[key];
if ([value isKindOfClass:[NSString class]] && ((NSString *)value).length > 0) {
return (NSString *)value;
}
}
return nil;
}
/// 解析 audioURL
- (NSString *)p_parseAudioURLFromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) return nil;
id dataObj = json[@"data"];
if ([dataObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *data = (NSDictionary *)dataObj;
id audioUrlObj = data[@"audioUrl"] ?: data[@"url"];
if (audioUrlObj && ![audioUrlObj isKindOfClass:[NSNull class]] && [audioUrlObj isKindOfClass:[NSString class]]) {
return (NSString *)audioUrlObj;
}
}
return nil;
}
@end