// // KBAIHomeVC.m // keyBoard // // Created by Mac on 2026/1/26. // #import "KBAIHomeVC.h" #import "KBPersonaChatCell.h" #import "KBPersonaModel.h" #import "KBVoiceInputBar.h" #import "KBVoiceRecordManager.h" #import "KBVoiceToTextManager.h" #import "AiVM.h" #import "KBHUD.h" #import "KBChatLimitPopView.h" #import "KBVipPay.h" #import "KBUserSessionManager.h" #import "LSTPopView.h" #import "KBAIMessageVC.h" #import @interface KBAIHomeVC () /// 人设列表容器 @property (nonatomic, strong) UICollectionView *collectionView; /// 底部语音输入栏 @property (nonatomic, strong) KBVoiceInputBar *voiceInputBar; @property (nonatomic, strong) MASConstraint *voiceInputBarBottomConstraint; @property (nonatomic, assign) CGFloat voiceInputBarHeight; @property (nonatomic, assign) CGFloat baseInputBarBottomSpacing; @property (nonatomic, assign) CGFloat currentKeyboardHeight; @property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap; @property (nonatomic, weak) LSTPopView *chatLimitPopView; /// 底部毛玻璃背景 @property (nonatomic, strong) UIView *bottomBackgroundView; @property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView; @property (nonatomic, strong) CAGradientLayer *bottomMaskLayer; /// 语音转写管理器 @property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager; /// 录音管理器 @property (nonatomic, strong) KBVoiceRecordManager *voiceRecordManager; /// 人设数据 @property (nonatomic, strong) NSMutableArray *personas; /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; /// 是否还有更多数据 @property (nonatomic, assign) BOOL hasMore; /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; /// 当前显示的索引 @property (nonatomic, assign) NSInteger currentIndex; /// 已预加载的索引集合 @property (nonatomic, strong) NSMutableSet *preloadedIndexes; /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; /// 是否正在等待 AI 回复(用于禁止滚动) @property (nonatomic, assign) BOOL isWaitingForAIResponse; /// 右上角消息按钮 @property (nonatomic, strong) UIButton *messageButton; @end @implementation KBAIHomeVC #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.kb_navView.hidden = true; // 初始化数据 self.personas = [NSMutableArray array]; self.currentPage = 1; self.hasMore = YES; self.isLoading = NO; self.currentIndex = 0; self.preloadedIndexes = [NSMutableSet set]; self.aiVM = [[AiVM alloc] init]; self.isWaitingForAIResponse = NO; // 初始化状态 [self setupUI]; [self setupVoiceToTextManager]; [self setupVoiceRecordManager]; [self setupKeyboardNotifications]; [self setupKeyboardDismissGesture]; [self loadPersonas]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (self.bottomMaskLayer) { self.bottomMaskLayer.frame = self.bottomBlurEffectView.bounds; } } #pragma mark - 1:控件初始化 - (void)setupUI { self.voiceInputBarHeight = 80.0; self.baseInputBarBottomSpacing = KB_TABBAR_HEIGHT; [self.view addSubview:self.collectionView]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 右上角消息按钮 [self.view addSubview:self.messageButton]; [self.messageButton mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(KB_STATUSBAR_HEIGHT + 10); make.right.equalTo(self.view).offset(-16); make.width.height.mas_equalTo(32); }]; // 底部毛玻璃背景 [self.view addSubview:self.bottomBackgroundView]; [self.bottomBackgroundView addSubview:self.bottomBlurEffectView]; [self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); // self.bottomBackgroundBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing); make.bottom.equalTo(self.view); make.height.mas_equalTo(self.voiceInputBarHeight); }]; [self.bottomBlurEffectView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.bottomBackgroundView); }]; // 底部语音输入栏 [self.view addSubview:self.voiceInputBar]; [self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing); make.height.mas_equalTo(self.voiceInputBarHeight); // 根据实际需要调整高度 }]; } #pragma mark - 2:数据加载 - (void)loadPersonas { if (self.isLoading) { return; } self.isLoading = YES; __weak typeof(self) weakSelf = self; [self.aiVM fetchPersonasWithPageNum:self.currentPage pageSize:10 completion:^(KBPersonaPageModel * _Nullable pageModel, NSError * _Nullable error) { weakSelf.isLoading = NO; if (error) { NSLog(@"加载人设列表失败:%@", error.localizedDescription); // TODO: 显示错误提示 return; } if (!pageModel || !pageModel.records) { NSLog(@"人设列表数据为空"); return; } // 追加数据 [weakSelf.personas addObjectsFromArray:pageModel.records]; weakSelf.hasMore = pageModel.hasMore; // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.collectionView reloadData]; // 首次加载,预加载前 3 个 if (weakSelf.currentPage == 1) { [weakSelf preloadDataForIndexes:@[@0, @1, @2]]; } }); NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@", weakSelf.personas.count, pageModel.total, pageModel.hasMore ? @"是" : @"否"); }]; } - (void)loadMorePersonas { if (!self.hasMore || self.isLoading) { return; } self.currentPage++; [self loadPersonas]; } #pragma mark - 3:预加载逻辑 - (void)preloadAdjacentCellsForIndex:(NSInteger)index { if (index < 0 || index >= self.personas.count) { return; } NSMutableArray *indexesToPreload = [NSMutableArray array]; // 上一个 if (index > 0) { [indexesToPreload addObject:@(index - 1)]; } // 当前 [indexesToPreload addObject:@(index)]; // 下一个 if (index < self.personas.count - 1) { [indexesToPreload addObject:@(index + 1)]; } [self preloadDataForIndexes:indexesToPreload]; } - (void)preloadDataForIndexes:(NSArray *)indexes { for (NSNumber *indexNum in indexes) { if ([self.preloadedIndexes containsObject:indexNum]) { continue; } [self.preloadedIndexes addObject:indexNum]; NSInteger index = [indexNum integerValue]; if (index >= self.personas.count) { continue; } NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0]; KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; if (cell) { [cell preloadDataIfNeeded]; } NSLog(@"预加载第 %ld 个人设", (long)index); } } #pragma mark - UICollectionViewDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.personas.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath]; cell.persona = self.personas[indexPath.item]; [self updateChatViewBottomInset]; // 标记为已预加载 [self.preloadedIndexes addObject:@(indexPath.item)]; // 直接触发预加载 [cell preloadDataIfNeeded]; return cell; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // 关键修复:如果正在等待 AI 回复,不进行预加载等操作 if (self.isWaitingForAIResponse) { return; } CGFloat pageHeight = scrollView.bounds.size.height; CGFloat offsetY = scrollView.contentOffset.y; NSInteger currentPage = offsetY / pageHeight; // 滑动超过 30% 就预加载 if (fmod(offsetY, pageHeight) > pageHeight * 0.3) { [self preloadAdjacentCellsForIndex:currentPage + 1]; } else { [self preloadAdjacentCellsForIndex:currentPage]; } // 接近底部时加载更多 if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) { [self loadMorePersonas]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { CGFloat pageHeight = scrollView.bounds.size.height; NSInteger currentPage = scrollView.contentOffset.y / pageHeight; self.currentIndex = currentPage; if (currentPage < self.personas.count) { NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name); } [self updateChatViewBottomInset]; } /// 关键修复:禁止在等待 AI 回复时开始拖拽 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { if (self.isWaitingForAIResponse) { NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动"); // 强制停止滚动 scrollView.scrollEnabled = NO; scrollView.scrollEnabled = YES; } } #pragma mark - 4:语音转写 - (void)setupVoiceToTextManager { self.voiceToTextManager = [[KBVoiceToTextManager alloc] initWithInputBar:self.voiceInputBar]; self.voiceToTextManager.delegate = self; self.voiceToTextManager.deepgramEnabled = NO; [self.voiceToTextManager prepareConnection]; } /// 5:录音管理 - (void)setupVoiceRecordManager { self.voiceRecordManager = [[KBVoiceRecordManager alloc] init]; self.voiceRecordManager.delegate = self; self.voiceRecordManager.minRecordDuration = 1.0; } #pragma mark - 6:键盘监听 - (void)setupKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; } - (void)handleKeyboardWillChangeFrame:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16); // 将键盘的 frame 转换到当前 view 的坐标系 CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil]; CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame)); self.currentKeyboardHeight = keyboardHeight; NSLog(@"[KBAIHomeVC] 键盘高度: %.2f, 屏幕高度: %.2f, 键盘 Y: %.2f", keyboardHeight, CGRectGetMaxY(self.view.bounds), CGRectGetMinY(convertedFrame)); // 问题1修复:计算 VoiceInputBar 应该距离 view.bottom 多远 CGFloat bottomSpacing; if (keyboardHeight > 0.0) { // 键盘弹起时:让 VoiceInputBar 紧贴键盘上方(距离 5px) // bottomSpacing = 键盘高度 - 5px(因为是负的 offset) bottomSpacing = keyboardHeight - 5.0; } else { // 键盘隐藏时:恢复到原始位置 bottomSpacing = self.baseInputBarBottomSpacing; } NSLog(@"[KBAIHomeVC] VoiceInputBar bottomSpacing: %.2f", bottomSpacing); [self.voiceInputBarBottomConstraint setOffset:-bottomSpacing]; // 问题2修复:键盘弹起时更新 ChatView 的 bottomInset [self updateChatViewBottomInset]; [UIView animateWithDuration:duration delay:0 options:options animations:^{ [self.view layoutIfNeeded]; } completion:nil]; } #pragma mark - 7:键盘收起 - (void)setupKeyboardDismissGesture { self.dismissKeyboardTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundTap)]; self.dismissKeyboardTap.cancelsTouchesInView = NO; self.dismissKeyboardTap.delegate = self; [self.view addGestureRecognizer:self.dismissKeyboardTap]; } - (void)handleBackgroundTap { [self.view endEditing:YES]; } #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ([touch.view isDescendantOfView:self.voiceInputBar]) { return NO; } return YES; } - (NSInteger)currentCompanionId { if (self.personas.count == 0) { return 0; } NSInteger index = self.currentIndex; if (index < 0 || index >= self.personas.count) { NSIndexPath *indexPath = self.collectionView.indexPathsForVisibleItems.firstObject; if (indexPath) { index = indexPath.item; } else { index = 0; } } KBPersonaModel *persona = self.personas[index]; return persona.personaId; } - (KBPersonaChatCell *)currentPersonaCell { if (self.personas.count == 0) { return nil; } NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0]; KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; if (cell) { return cell; } for (NSIndexPath *visibleIndex in self.collectionView.indexPathsForVisibleItems) { KBPersonaChatCell *visibleCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:visibleIndex]; if (visibleCell) { return visibleCell; } } return nil; } #pragma mark - Private - (void)updateChatViewBottomInset { // 键盘弹起时,增加 bottomInset 让最后一条消息显示在 VoiceInputBar 上方 CGFloat bottomInset; if (self.currentKeyboardHeight > 0.0) { // 键盘弹起时: // avatarImageView 距离屏幕底部的距离 = KB_TABBAR_HEIGHT + 50 + 20 // chatView 物理底部到屏幕底部 = KB_TABBAR_HEIGHT + 50 + 20 + 54(头像) + 10(间距) ≈ 153 // 但是 VoiceInputBar 和键盘会遮挡 chatView // 需要的 bottomInset = 键盘高度 + VoiceInputBar 高度 - chatView 已经避开的底部区域 CGFloat avatarBottomSpace = KB_TABBAR_HEIGHT + 50 + 20; // avatarImageView 距离底部的距离 CGFloat chatViewPhysicalBottomSpace = avatarBottomSpace + 54 + 10; // chatView 物理底部距离屏幕底部 // 需要抬高的额外距离 = (键盘 + InputBar) - chatView 已经避开的空间 bottomInset = (self.currentKeyboardHeight + self.voiceInputBarHeight) - chatViewPhysicalBottomSpace; // 确保不会是负数 bottomInset = MAX(bottomInset, 0); NSLog(@"[KBAIHomeVC] 键盘弹起 - bottomInset: %.2f (键盘: %.2f + InputBar: %.2f - 已避开: %.2f)", bottomInset, self.currentKeyboardHeight, self.voiceInputBarHeight, chatViewPhysicalBottomSpace); } else { // 键盘隐藏时:不需要额外的 bottomInset bottomInset = 0; NSLog(@"[KBAIHomeVC] 键盘隐藏 - bottomInset: %.2f", bottomInset); } for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) { KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; if (cell) { [cell updateChatViewBottomInset:bottomInset]; // 键盘弹起时,自动滚动到最后一条消息 if (self.currentKeyboardHeight > 0.0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [cell.chatView scrollToBottom]; }); } } } } - (void)showChatLimitPopWithMessage:(NSString *)message { if (self.chatLimitPopView) { [self.chatLimitPopView dismiss]; } CGFloat width = KB_SCREEN_WIDTH - 60; KBChatLimitPopView *content = [[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)]; content.message = message; content.delegate = self; LSTPopView *pop = [LSTPopView initWithCustomView:content parentView:nil popStyle:LSTPopStyleFade dismissStyle:LSTDismissStyleFade]; pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; pop.hemStyle = LSTHemStyleCenter; pop.isClickBgDismiss = YES; pop.isAvoidKeyboard = NO; self.chatLimitPopView = pop; [pop pop]; } #pragma mark - Lazy Load - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; layout.minimumLineSpacing = 0; layout.minimumInteritemSpacing = 0; layout.itemSize = [UIScreen mainScreen].bounds.size; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.pagingEnabled = YES; _collectionView.showsVerticalScrollIndicator = NO; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.delegate = self; _collectionView.dataSource = self; [_collectionView registerClass:[KBPersonaChatCell class] forCellWithReuseIdentifier:@"KBPersonaChatCell"]; if (@available(iOS 11.0, *)) { _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } } return _collectionView; } - (KBVoiceInputBar *)voiceInputBar { if (!_voiceInputBar) { _voiceInputBar = [[KBVoiceInputBar alloc] init]; _voiceInputBar.statusText = @"按住按钮开始对话"; } return _voiceInputBar; } #pragma mark - KBChatLimitPopViewDelegate - (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view { [self.chatLimitPopView dismiss]; } - (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view { [self.chatLimitPopView dismiss]; if (![KBUserSessionManager shared].isLoggedIn) { [[KBUserSessionManager shared] goLoginVC]; return; } KBVipPay *vc = [[KBVipPay alloc] init]; [KB_CURRENT_NAV pushViewController:vc animated:true]; } - (UIView *)bottomBackgroundView { if (!_bottomBackgroundView) { _bottomBackgroundView = [[UIView alloc] init]; _bottomBackgroundView.clipsToBounds = YES; } return _bottomBackgroundView; } - (UIVisualEffectView *)bottomBlurEffectView { if (!_bottomBlurEffectView) { UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; _bottomBlurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; _bottomBlurEffectView.layer.mask = self.bottomMaskLayer; } return _bottomBlurEffectView; } - (CAGradientLayer *)bottomMaskLayer { if (!_bottomMaskLayer) { _bottomMaskLayer = [CAGradientLayer layer]; _bottomMaskLayer.startPoint = CGPointMake(0.5, 1); _bottomMaskLayer.endPoint = CGPointMake(0.5, 0); _bottomMaskLayer.colors = @[ (__bridge id)[UIColor whiteColor].CGColor, (__bridge id)[UIColor whiteColor].CGColor, (__bridge id)[UIColor clearColor].CGColor ]; _bottomMaskLayer.locations = @[@(0.0), @(0.5), @(1.0)]; } return _bottomMaskLayer; } - (UIButton *)messageButton { if (!_messageButton) { _messageButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_messageButton setImage:[UIImage imageNamed:@"ai_message_icon"] forState:UIControlStateNormal]; [_messageButton addTarget:self action:@selector(messageButtonTapped) forControlEvents:UIControlEventTouchUpInside]; } return _messageButton; } #pragma mark - Actions - (void)messageButtonTapped { KBAIMessageVC *vc = [[KBAIMessageVC alloc] init]; [self.navigationController pushViewController:vc animated:YES]; } #pragma mark - KBVoiceToTextManagerDelegate - (void)voiceToTextManager:(KBVoiceToTextManager *)manager didReceiveFinalText:(NSString *)text { [self handleTranscribedText:text]; } - (void)voiceToTextManager:(KBVoiceToTextManager *)manager didFailWithError:(NSError *)error { NSLog(@"[KBAIHomeVC] 语音识别失败:%@", error.localizedDescription); } - (void)voiceToTextManagerDidBeginRecording:(KBVoiceToTextManager *)manager { [self.voiceRecordManager startRecording]; } - (void)voiceToTextManagerDidEndRecording:(KBVoiceToTextManager *)manager { [self.voiceRecordManager stopRecording]; } - (void)voiceToTextManagerDidCancelRecording:(KBVoiceToTextManager *)manager { [self.voiceRecordManager cancelRecording]; } #pragma mark - KBVoiceRecordManagerDelegate - (void)voiceRecordManager:(KBVoiceRecordManager *)manager didFinishRecordingAtURL:(NSURL *)fileURL duration:(NSTimeInterval)duration { NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fileURL.path error:nil]; unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue]; NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize); __weak typeof(self) weakSelf = self; [self.aiVM transcribeAudioFileAtURL:fileURL completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_async(dispatch_get_main_queue(), ^{ if (error) { NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription); [KBHUD showError:KBLocalized(@"语音转文字失败,请重试")]; return; } NSString *transcript = response.data.transcript ?: @""; if (transcript.length == 0) { NSLog(@"[KBAIHomeVC] 语音转文字结果为空"); [KBHUD showError:KBLocalized(@"未识别到语音内容")]; return; } [strongSelf handleTranscribedText:transcript]; }); }]; } - (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager { NSLog(@"[KBAIHomeVC] 录音过短,已忽略"); [KBHUD showError:KBLocalized(@"录音时间过短,请重新录音")]; } - (void)voiceRecordManager:(KBVoiceRecordManager *)manager didFailWithError:(NSError *)error { NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription); } #pragma mark - Private - (void)handleTranscribedText:(NSString *)text { if (text.length == 0) { return; } NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text); NSInteger companionId = [self currentCompanionId]; if (companionId <= 0) { NSLog(@"[KBAIHomeVC] companionId 无效,取消请求"); return; } KBPersonaChatCell *currentCell = [self currentPersonaCell]; if (currentCell) { [currentCell appendUserMessage:text]; } // 关键修复:发送消息前禁止 CollectionView 滚动 self.isWaitingForAIResponse = YES; self.collectionView.scrollEnabled = NO; NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动"); __weak typeof(self) weakSelf = self; [self.aiVM requestChatMessageWithContent:text companionId:companionId completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_async(dispatch_get_main_queue(), ^{ // 关键修复:收到响应后(无论成功或失败)重新启用 CollectionView 滚动 strongSelf.isWaitingForAIResponse = NO; strongSelf.collectionView.scrollEnabled = YES; NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); if (response.code == 50030) { NSString *message = response.message ?: @""; [strongSelf showChatLimitPopWithMessage:message]; return; } if (!response || !response.data) { NSString *message = response.message ?: @"聊天响应为空"; NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message); if (message.length > 0) { [KBHUD showError:message]; } return; } NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @""; NSString *audioId = response.data.audioId; if (aiResponse.length == 0) { NSLog(@"[KBAIHomeVC] AI 回复为空"); return; } KBPersonaChatCell *cell = [strongSelf currentPersonaCell]; if (cell) { [cell appendAssistantMessage:aiResponse audioId:audioId]; } }); }]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end