简单封装

This commit is contained in:
2026-01-30 13:26:02 +08:00
parent 36774a8a2c
commit 3705db4aab
4 changed files with 527 additions and 293 deletions

View File

@@ -24,6 +24,7 @@
#import "KBSkinManager.h"
#import "KBSuggestionEngine.h"
#import "KBNetworkManager.h"
#import "KBVM.h"
#import "Masonry.h"
#import "UIImage+KBColor.h"
#import <AVFoundation/AVFoundation.h>
@@ -93,7 +94,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
@property(nonatomic, strong) NSMutableArray<KBChatMessage *> *chatMessages;
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
@property(nonatomic, strong) NSCache<NSString *, UIImage *> *chatAvatarCache;
@property(nonatomic, assign) BOOL chatPanelVisible;
@end
@@ -918,37 +918,16 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
if (urlString.length == 0) {
return;
}
UIImage *cached = [self.chatAvatarCache objectForKey:urlString];
if (cached) {
message.avatarImage = cached;
[self kb_reloadChatRowForMessage:message];
return;
}
if (![[KBFullAccessManager shared] hasFullAccess]) {
return;
}
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared]
GETData:urlString
parameters:nil
headers:nil
completion:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error || data.length == 0) {
return;
}
UIImage *image = [UIImage imageWithData:data];
if (!image) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
return;
}
[self.chatAvatarCache setObject:image forKey:urlString];
message.avatarImage = image;
[self kb_reloadChatRowForMessage:message];
});
[[KBVM shared] downloadAvatarFromURL:urlString completion:^(UIImage *image, NSError *error) {
__strong typeof(weakSelf) self = weakSelf;
if (!self || !image) return;
message.avatarImage = image;
[self kb_reloadChatRowForMessage:message];
}];
}
@@ -1042,292 +1021,119 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
}
// AppGroup persona companionId
NSInteger companionId = [self kb_selectedCompanionId];
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)
};
NSLog(@"[Keyboard] 发送聊天请求: path=%@, companionId=%ld", path, (long)companionId);
NSInteger companionId = [[KBVM shared] selectedCompanionIdFromAppGroup];
NSLog(@"[Keyboard] 发送聊天请求: companionId=%ld", (long)companionId);
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared] POST:path
jsonBody:params
headers:nil
completion:^(NSDictionary *json, NSURLResponse *response,
NSError *error) {
NSLog(@"[Keyboard] ========== 聊天响应回调 ==========");
NSLog(@"[Keyboard] error: %@", error);
NSLog(@"[Keyboard] json: %@", json);
[[KBVM shared] sendChatMessageWithContent:content
companionId:companionId
completion:^(KBChatResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
NSLog(@"[Keyboard] ❌ self 为空");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
NSLog(@"[Keyboard] ❌ self 为空");
return;
}
NSLog(@"[Keyboard] 回调中 chatPanelView=%p", self.chatPanelView);
NSLog(@"[Keyboard] 回调中 chatPanelView=%p", self.chatPanelView);
if (!response.success) {
NSLog(@"[Keyboard] ❌ 请求失败: %@", response.errorMessage);
[self.chatPanelView kb_removeLoadingAssistantMessage];
[KBHUD showInfo:response.errorMessage ?: KBLocalized(@"请求失败")];
return;
}
if (error) {
NSLog(@"[Keyboard] ❌ 请求失败: %@", error.localizedDescription);
[self.chatPanelView kb_removeLoadingAssistantMessage];
NSString *tip = error.localizedDescription ?: KBLocalized(@"请求失败");
[KBHUD showInfo:tip];
return;
}
NSLog(@"[Keyboard] ✅ 解析结果: text=%@, audioId=%@", response.text, response.audioId);
//
NSString *text = [self kb_chatMessageTextFromJSON:json];
NSString *audioId = [self kb_chatMessageAudioIdFromJSON:json];
if (response.text.length == 0) {
NSLog(@"[Keyboard] ❌ 文本为空,移除 loading");
[self.chatPanelView kb_removeLoadingAssistantMessage];
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
return;
}
NSLog(@"[Keyboard] ✅ 解析结果: text=%@, audioId=%@", text, audioId);
NSLog(@"[Keyboard] 准备调用 kb_addAssistantMessage, chatPanelView=%p", self.chatPanelView);
// AI
[self.chatPanelView kb_addAssistantMessage:response.text audioId:response.audioId];
NSLog(@"[Keyboard] kb_addAssistantMessage 调用完成");
if (text.length == 0) {
NSLog(@"[Keyboard] ❌ 文本为空,移除 loading");
[self.chatPanelView kb_removeLoadingAssistantMessage];
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
return;
}
NSLog(@"[Keyboard] 准备调用 kb_addAssistantMessage, chatPanelView=%p", self.chatPanelView);
// AI
[self.chatPanelView kb_addAssistantMessage:text audioId:audioId];
NSLog(@"[Keyboard] kb_addAssistantMessage 调用完成");
// audioId
if (audioId.length > 0) {
NSDate *startTime = [NSDate date];
[self kb_preloadAudioWithAudioId:audioId startTime:startTime];
}
});
// audioId
if (response.audioId.length > 0) {
[self kb_preloadAudioWithAudioId:response.audioId];
}
}];
}
/// AppGroup persona companionId
- (NSInteger)kb_selectedCompanionId {
NSDictionary *persona = [self kb_selectedPersonaFromAppGroup];
if (persona) {
// App personaId
id companionIdObj = persona[@"personaId"] ?: persona[@"companionId"] ?: persona[@"id"];
if ([companionIdObj respondsToSelector:@selector(integerValue)]) {
NSInteger companionId = [companionIdObj integerValue];
NSLog(@"[Keyboard] 从 AppGroup 获取 companionId: %ld", (long)companionId);
return companionId;
}
}
NSLog(@"[Keyboard] 未找到 persona使用默认 companionId: 0");
return 0; //
}
///
- (NSString *)kb_chatMessageTextFromJSON:(NSDictionary *)json {
NSLog(@"[Keyboard] ========== kb_chatMessageTextFromJSON ==========");
NSLog(@"[Keyboard] 输入 json 类型: %@", NSStringFromClass([json class]));
if (![json isKindOfClass:[NSDictionary class]]) {
NSLog(@"[Keyboard] ❌ json 不是字典类型");
return @"";
}
id dataObj = json[@"data"];
NSLog(@"[Keyboard] data 字段类型: %@, 值: %@", NSStringFromClass([dataObj class]), dataObj);
if ([dataObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *data = (NSDictionary *)dataObj;
NSLog(@"[Keyboard] data 字典内容: %@", data);
// aiResponse
NSArray *dataKeys = @[@"aiResponse", @"content", @"text", @"message"];
for (NSString *key in dataKeys) {
id value = data[key];
NSLog(@"[Keyboard] 检查 data.%@ = %@ (类型: %@)", key, value, NSStringFromClass([value class]));
if ([value isKindOfClass:[NSString class]] && ((NSString *)value).length > 0) {
NSLog(@"[Keyboard] ✅ 从 data.%@ 解析到文本: %@", key, value);
return (NSString *)value;
}
}
NSLog(@"[Keyboard] ❌ data 字典中没有找到有效文本");
} else if ([dataObj isKindOfClass:[NSString class]]) {
NSLog(@"[Keyboard] data 是字符串: %@", dataObj);
return (NSString *)dataObj;
} else {
NSLog(@"[Keyboard] ❌ data 字段类型不支持: %@", NSStringFromClass([dataObj class]));
}
return @"";
}
/// audioId
- (NSString *)kb_chatMessageAudioIdFromJSON:(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;
return [[KBVM shared] selectedCompanionIdFromAppGroup];
}
#pragma mark - Audio Preload
/// audioURL
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId startTime:(NSDate *)startTime {
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId {
if (audioId.length == 0) return;
NSLog(@"[Keyboard] 开始预加载音频audioId: %@", audioId);
// 10110
[self kb_pollAudioURLWithAudioId:audioId retryCount:0 maxRetries:10 startTime:startTime];
}
/// audioURL
- (void)kb_pollAudioURLWithAudioId:(NSString *)audioId
retryCount:(NSInteger)retryCount
maxRetries:(NSInteger)maxRetries
startTime:(NSDate *)startTime {
NSString *path = [NSString stringWithFormat:@"/chat/audio/%@", audioId];
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared] GET:path
parameters:nil
headers:nil
completion:^(NSDictionary *json, NSURLResponse *response,
NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) return;
[[KBVM shared] pollAudioURLWithAudioId:audioId
maxRetries:10
interval:1.0
completion:^(KBAudioResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self) return;
// audioURL
NSString *audioURL = nil;
if ([json isKindOfClass:[NSDictionary class]]) {
id dataObj = json[@"data"];
if ([dataObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *dataDict = (NSDictionary *)dataObj;
id audioUrlObj = dataDict[@"audioUrl"] ?: dataDict[@"url"];
if (audioUrlObj && ![audioUrlObj isKindOfClass:[NSNull class]] && [audioUrlObj isKindOfClass:[NSString class]]) {
audioURL = (NSString *)audioUrlObj;
}
}
}
if (!response.success || response.audioURL.length == 0) {
NSLog(@"[Keyboard] ❌ 预加载音频 URL 获取失败: %@", response.errorMessage);
return;
}
// audioURL
if (audioURL.length > 0) {
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[Keyboard] ✅ 预加载音频 URL 获取成功(第 %ld 次),耗时: %.2f 秒", (long)(retryCount + 1), elapsed);
//
[self kb_downloadPreloadAudioFromURL:audioURL startTime:startTime];
NSLog(@"[Keyboard] ✅ 预加载音频 URL 获取成功");
//
[[KBVM shared] downloadAudioFromURL:response.audioURL
completion:^(KBAudioResponse *audioResponse) {
if (!audioResponse.success) {
NSLog(@"[Keyboard] ❌ 预加载音频下载失败: %@", audioResponse.errorMessage);
return;
}
//
if (retryCount < maxRetries - 1) {
NSLog(@"[Keyboard] 预加载音频未就绪1秒后重试 (%ld/%ld)", (long)(retryCount + 1), (long)maxRetries);
// 1
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self kb_pollAudioURLWithAudioId:audioId
retryCount:retryCount + 1
maxRetries:maxRetries
startTime:startTime];
});
} else {
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[Keyboard] ❌ 预加载音频失败,已重试 %ld 次,总耗时: %.2f 秒", (long)maxRetries, elapsed);
}
});
}];
}
///
- (void)kb_downloadPreloadAudioFromURL:(NSString *)urlString startTime:(NSDate *)startTime {
if (urlString.length == 0) return;
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared] GETData:urlString
parameters:nil
headers:nil
completion:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) return;
if (error || !data || data.length == 0) {
NSLog(@"[Keyboard] 预加载:下载音频失败: %@", error.localizedDescription ?: @"");
return;
}
//
NSError *playerError = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:&playerError];
NSTimeInterval duration = 0;
if (!playerError && player) {
duration = player.duration;
}
// AI
[self.chatPanelView kb_updateLastAssistantMessageWithAudioData:data duration:duration];
NSTimeInterval totalElapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[Keyboard] ✅ 预加载音频完成,音频时长: %.2f秒,总耗时: %.2f 秒", duration, totalElapsed);
});
[self.chatPanelView kb_updateLastAssistantMessageWithAudioData:audioResponse.audioData
duration:audioResponse.duration];
NSLog(@"[Keyboard] ✅ 预加载音频完成,音频时长: %.2f秒", audioResponse.duration);
}];
}];
}
- (void)kb_downloadChatAudioFromURL:(NSString *)audioURL
displayText:(NSString *)displayText {
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared]
GETData:audioURL
parameters:nil
headers:nil
completion:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
return;
}
if (error) {
NSString *tip = error.localizedDescription ?: KBLocalized(@"下载失败");
[KBHUD showInfo:tip];
return;
}
if (data.length == 0) {
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
return;
}
NSString *ext = @"m4a";
NSURL *url = [NSURL URLWithString:audioURL];
if (url.pathExtension.length > 0) {
ext = url.pathExtension;
}
[self kb_handleChatAudioData:data
fileExtension:ext
displayText:displayText];
});
}];
[[KBVM shared] downloadAudioFromURL:audioURL completion:^(KBAudioResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self) return;
if (!response.success) {
[KBHUD showInfo:response.errorMessage ?: KBLocalized(@"下载失败")];
return;
}
if (!response.audioData || response.audioData.length == 0) {
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
return;
}
NSString *ext = @"m4a";
NSURL *url = [NSURL URLWithString:audioURL];
if (url.pathExtension.length > 0) {
ext = url.pathExtension;
}
[self kb_handleChatAudioData:response.audioData
fileExtension:ext
displayText:displayText];
}];
}
- (void)kb_handleChatAudioData:(NSData *)data
@@ -1605,13 +1411,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
return _chatMessages;
}
- (NSCache<NSString *, UIImage *> *)chatAvatarCache {
if (!_chatAvatarCache) {
_chatAvatarCache = [[NSCache alloc] init];
}
return _chatAvatarCache;
}
- (KBKeyboardSubscriptionView *)subscriptionView {
if (!_subscriptionView) {
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];

95
CustomKeyboard/VM/KBVM.h Normal file
View File

@@ -0,0 +1,95 @@
//
// KBVM.h
// CustomKeyboard
//
// 键盘扩展的 ViewModel封装网络请求逻辑
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 聊天响应模型
@interface KBChatResponse : NSObject
@property (nonatomic, copy, nullable) NSString *text;
@property (nonatomic, copy, nullable) NSString *audioId;
@property (nonatomic, copy, nullable) NSString *errorMessage;
@property (nonatomic, assign) BOOL success;
@end
/// 音频响应模型
@interface KBAudioResponse : NSObject
@property (nonatomic, copy, nullable) NSString *audioURL;
@property (nonatomic, strong, nullable) NSData *audioData;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, copy, nullable) NSString *errorMessage;
@property (nonatomic, assign) BOOL success;
@end
/// 聊天请求回调
typedef void(^KBChatCompletion)(KBChatResponse *response);
/// 音频 URL 回调
typedef void(^KBAudioURLCompletion)(KBAudioResponse *response);
/// 音频数据回调
typedef void(^KBAudioDataCompletion)(KBAudioResponse *response);
/// 头像回调
typedef void(^KBAvatarCompletion)(UIImage * _Nullable image, NSError * _Nullable error);
@interface KBVM : NSObject
+ (instancetype)shared;
#pragma mark - Chat API
/// 发送聊天消息
/// @param content 消息内容
/// @param companionId 人设 ID
/// @param completion 回调
- (void)sendChatMessageWithContent:(NSString *)content
companionId:(NSInteger)companionId
completion:(KBChatCompletion)completion;
#pragma mark - Audio API
/// 获取音频 URL单次请求
/// @param audioId 音频 ID
/// @param completion 回调
- (void)fetchAudioURLWithAudioId:(NSString *)audioId
completion:(KBAudioURLCompletion)completion;
/// 轮询获取音频 URL自动重试
/// @param audioId 音频 ID
/// @param maxRetries 最大重试次数
/// @param interval 重试间隔(秒)
/// @param completion 回调
- (void)pollAudioURLWithAudioId:(NSString *)audioId
maxRetries:(NSInteger)maxRetries
interval:(NSTimeInterval)interval
completion:(KBAudioURLCompletion)completion;
/// 下载音频数据
/// @param urlString 音频 URL
/// @param completion 回调
- (void)downloadAudioFromURL:(NSString *)urlString
completion:(KBAudioDataCompletion)completion;
#pragma mark - Avatar API
/// 下载头像图片
/// @param urlString 头像 URL
/// @param completion 回调
- (void)downloadAvatarFromURL:(NSString *)urlString
completion:(KBAvatarCompletion)completion;
#pragma mark - Helper
/// 从 AppGroup 获取选中的 persona companionId
- (NSInteger)selectedCompanionIdFromAppGroup;
/// 从 AppGroup 获取选中的 persona 信息
- (nullable NSDictionary *)selectedPersonaFromAppGroup;
@end
NS_ASSUME_NONNULL_END

334
CustomKeyboard/VM/KBVM.m Normal file
View File

@@ -0,0 +1,334 @@
//
// 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

View File

@@ -23,6 +23,7 @@
04122FAD2EC73C0100EF7AB3 /* KBVipSubscribeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122FAC2EC73C0100EF7AB3 /* KBVipSubscribeCell.m */; };
04122FB02EC73C0100EF7AB3 /* KBVipReviewItemCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122FAF2EC73C0100EF7AB3 /* KBVipReviewItemCell.m */; };
04122FB32EC73C0100EF7AB3 /* KBVipReviewListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122FB22EC73C0100EF7AB3 /* KBVipReviewListCell.m */; };
0419C9662F2C7693002E86D3 /* KBVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0419C9652F2C7693002E86D3 /* KBVM.m */; };
04286A002ECAEF2B00CE730C /* KBMoneyBtn.m in Sources */ = {isa = PBXBuildFile; fileRef = 042869FE2ECAEF2B00CE730C /* KBMoneyBtn.m */; };
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A022ECB0A1600CE730C /* KBSexSelVC.m */; };
04286A062ECC81B200CE730C /* KBSkinService.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A052ECC81B200CE730C /* KBSkinService.m */; };
@@ -357,6 +358,8 @@
04122FAF2EC73C0100EF7AB3 /* KBVipReviewItemCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVipReviewItemCell.m; sourceTree = "<group>"; };
04122FB12EC73C0100EF7AB3 /* KBVipReviewListCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVipReviewListCell.h; sourceTree = "<group>"; };
04122FB22EC73C0100EF7AB3 /* KBVipReviewListCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVipReviewListCell.m; sourceTree = "<group>"; };
0419C9642F2C7693002E86D3 /* KBVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVM.h; sourceTree = "<group>"; };
0419C9652F2C7693002E86D3 /* KBVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVM.m; sourceTree = "<group>"; };
042869FD2ECAEF2B00CE730C /* KBMoneyBtn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMoneyBtn.h; sourceTree = "<group>"; };
042869FE2ECAEF2B00CE730C /* KBMoneyBtn.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMoneyBtn.m; sourceTree = "<group>"; };
04286A012ECB0A1600CE730C /* KBSexSelVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSexSelVC.h; sourceTree = "<group>"; };
@@ -975,6 +978,8 @@
0419C9632F2C7630002E86D3 /* VM */ = {
isa = PBXGroup;
children = (
0419C9642F2C7693002E86D3 /* KBVM.h */,
0419C9652F2C7693002E86D3 /* KBVM.m */,
);
path = VM;
sourceTree = "<group>";
@@ -2335,6 +2340,7 @@
A1B2C9272FC9000100000001 /* KBChatMessageCell.m in Sources */,
A1B2C9282FC9000100000001 /* KBChatPanelView.m in Sources */,
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */,
0419C9662F2C7693002E86D3 /* KBVM.m in Sources */,
048FFD512F2B68F7005D62AE /* KBPersonaModel.m in Sources */,
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
04FEDAB32EEDB05000123456 /* KBEmojiPanelView.m in Sources */,