处理键盘
This commit is contained in:
@@ -802,18 +802,26 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||||
}
|
||||
|
||||
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
||||
for (KBChatMessage *msg in self.chatMessages) {
|
||||
if (msg.audioFilePath.length > 0) {
|
||||
NSString *tmpRoot = NSTemporaryDirectory();
|
||||
if (tmpRoot.length > 0 &&
|
||||
[msg.audioFilePath hasPrefix:tmpRoot]) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
|
||||
error:nil];
|
||||
}
|
||||
}
|
||||
- (void)chatPanelView:(KBChatPanelView *)view didTapVoiceButtonForMessage:(KBChatMessage *)message {
|
||||
if (!message) return;
|
||||
|
||||
// 如果有 audioData,直接播放
|
||||
if (message.audioData && message.audioData.length > 0) {
|
||||
[self kb_playChatAudioData:message.audioData];
|
||||
return;
|
||||
}
|
||||
[self.chatMessages removeAllObjects];
|
||||
|
||||
// 如果有 audioFilePath,播放文件
|
||||
if (message.audioFilePath.length > 0) {
|
||||
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[Keyboard] 没有音频数据可播放");
|
||||
}
|
||||
|
||||
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
||||
// 清空 chatPanelView 内部的消息
|
||||
[self.chatPanelView kb_reloadWithMessages:@[]];
|
||||
if (self.chatAudioPlayer.isPlaying) {
|
||||
[self.chatAudioPlayer stop];
|
||||
@@ -847,17 +855,25 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
KBChatMessage *outgoing =
|
||||
[KBChatMessage messageWithText:text outgoing:YES audioFilePath:nil];
|
||||
NSLog(@"[Keyboard] ========== kb_sendChatText ==========");
|
||||
NSLog(@"[Keyboard] chatPanelView=%p", self.chatPanelView);
|
||||
|
||||
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
|
||||
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
|
||||
[self kb_appendChatMessage:outgoing];
|
||||
[self.chatPanelView kb_addUserMessage:text];
|
||||
[self kb_prefetchAvatarForMessage:outgoing];
|
||||
|
||||
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||
[KBHUD showInfo:KBLocalized(@"请开启完全访问后使用")];
|
||||
return;
|
||||
}
|
||||
[self kb_requestChatAudioForText:text];
|
||||
|
||||
// 添加 loading 消息
|
||||
NSLog(@"[Keyboard] 准备添加 loading 消息,chatPanelView=%p", self.chatPanelView);
|
||||
[self.chatPanelView kb_addLoadingAssistantMessage];
|
||||
|
||||
// 调用新的聊天接口
|
||||
[self kb_requestChatMessageWithContent:text];
|
||||
}
|
||||
|
||||
- (void)kb_clearHostInputForText:(NSString *)text {
|
||||
@@ -937,23 +953,16 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
}
|
||||
|
||||
- (void)kb_reloadChatRowForMessage:(KBChatMessage *)message {
|
||||
NSUInteger idx = [self.chatMessages indexOfObject:message];
|
||||
if (idx == NSNotFound) {
|
||||
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
|
||||
return;
|
||||
}
|
||||
NSLog(@"[Keyboard] ========== kb_reloadChatRowForMessage ==========");
|
||||
// 不再使用 self.chatMessages,直接刷新 tableView
|
||||
UITableView *tableView = self.chatPanelView.tableView;
|
||||
if (!tableView) {
|
||||
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
|
||||
NSLog(@"[Keyboard] tableView 为空,跳过");
|
||||
return;
|
||||
}
|
||||
if (idx >= (NSUInteger)[tableView numberOfRowsInSection:0]) {
|
||||
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
|
||||
return;
|
||||
}
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:idx inSection:0];
|
||||
[tableView reloadRowsAtIndexPaths:@[ indexPath ]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
// 刷新整个 tableView
|
||||
NSLog(@"[Keyboard] 调用 tableView reloadData");
|
||||
[tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)kb_requestChatAudioForText:(NSString *)text {
|
||||
@@ -1019,6 +1028,274 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - New Chat API (with typewriter effect and audio preload)
|
||||
|
||||
/// 调用新的聊天接口(返回文本和 audioId)
|
||||
- (void)kb_requestChatMessageWithContent:(NSString *)content {
|
||||
NSLog(@"[Keyboard] ========== kb_requestChatMessageWithContent ==========");
|
||||
NSLog(@"[Keyboard] 请求内容: %@", content);
|
||||
|
||||
if (content.length == 0) {
|
||||
NSLog(@"[Keyboard] ❌ 内容为空,移除 loading");
|
||||
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 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);
|
||||
|
||||
__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);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
if (!self) {
|
||||
NSLog(@"[Keyboard] ❌ self 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[Keyboard] 回调中 chatPanelView=%p", self.chatPanelView);
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[Keyboard] ❌ 请求失败: %@", error.localizedDescription);
|
||||
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||||
NSString *tip = error.localizedDescription ?: KBLocalized(@"请求失败");
|
||||
[KBHUD showInfo:tip];
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析返回数据
|
||||
NSString *text = [self kb_chatMessageTextFromJSON:json];
|
||||
NSString *audioId = [self kb_chatMessageAudioIdFromJSON:json];
|
||||
|
||||
NSLog(@"[Keyboard] ✅ 解析结果: text=%@, audioId=%@", text, audioId);
|
||||
|
||||
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];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
/// 从 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;
|
||||
}
|
||||
|
||||
#pragma mark - Audio Preload
|
||||
|
||||
/// 预加载音频(轮询获取 audioURL)
|
||||
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId startTime:(NSDate *)startTime {
|
||||
if (audioId.length == 0) return;
|
||||
|
||||
NSLog(@"[Keyboard] 开始预加载音频,audioId: %@", audioId);
|
||||
|
||||
// 开始轮询(最多10次,每次间隔1秒,共10秒)
|
||||
[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;
|
||||
|
||||
// 解析 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果成功获取到 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];
|
||||
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);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)kb_downloadChatAudioFromURL:(NSString *)audioURL
|
||||
displayText:(NSString *)displayText {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
@@ -1217,6 +1494,48 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
[player play];
|
||||
}
|
||||
|
||||
/// 播放音频数据
|
||||
- (void)kb_playChatAudioData:(NSData *)audioData {
|
||||
if (!audioData || audioData.length == 0) {
|
||||
NSLog(@"[Keyboard] 音频数据为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果正在播放,先停止
|
||||
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
|
||||
[self.chatAudioPlayer stop];
|
||||
self.chatAudioPlayer = nil;
|
||||
}
|
||||
|
||||
// 配置音频会话
|
||||
NSError *sessionError = nil;
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
|
||||
[session setCategory:AVAudioSessionCategoryPlayback
|
||||
withOptions:AVAudioSessionCategoryOptionDuckOthers
|
||||
error:&sessionError];
|
||||
} else {
|
||||
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
|
||||
}
|
||||
[session setActive:YES error:nil];
|
||||
|
||||
// 创建播放器
|
||||
NSError *playerError = nil;
|
||||
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
|
||||
if (playerError || !player) {
|
||||
NSLog(@"[Keyboard] 音频播放器初始化失败: %@", playerError.localizedDescription);
|
||||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
||||
return;
|
||||
}
|
||||
|
||||
self.chatAudioPlayer = player;
|
||||
player.volume = 1.0;
|
||||
[player prepareToPlay];
|
||||
[player play];
|
||||
|
||||
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
|
||||
}
|
||||
|
||||
#pragma mark - KBKeyboardSubscriptionViewDelegate
|
||||
|
||||
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
|
||||
@@ -1272,6 +1591,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
|
||||
- (KBChatPanelView *)chatPanelView {
|
||||
if (!_chatPanelView) {
|
||||
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
||||
_chatPanelView = [[KBChatPanelView alloc] init];
|
||||
_chatPanelView.delegate = self;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user