From cc82396195752748c3e97075184c80ec6b0f0913 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 30 Jan 2026 21:24:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E6=9C=ACok?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Shared/KBConfig.h | 4 +- .../Class/AiTalk/V/Chat/KBChatTableView.h | 3 + .../Class/AiTalk/V/Chat/KBChatTableView.m | 174 +++++++++++++++++- .../Class/AiTalk/V/Chat/KBPersonaChatCell.m | 84 +++++---- keyBoard/Class/AiTalk/VC/KBAIHomeVC.m | 2 +- 5 files changed, 222 insertions(+), 45 deletions(-) diff --git a/Shared/KBConfig.h b/Shared/KBConfig.h index 7ee6a17..12f34e3 100644 --- a/Shared/KBConfig.h +++ b/Shared/KBConfig.h @@ -41,8 +41,8 @@ // 基础baseUrl #ifndef KB_BASE_URL //#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/" -#define KB_BASE_URL @"http://192.168.2.22:7529/api" -//#define KB_BASE_URL @"https://devcallback.loveamorkey.com/api" +//#define KB_BASE_URL @"http://192.168.2.22:7529/api" +#define KB_BASE_URL @"https://devcallback.loveamorkey.com/api" #endif #import "KBFont.h" diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h index 27b024e..8537fe8 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h @@ -81,6 +81,9 @@ NS_ASSUME_NONNULL_BEGIN keepOffset:(BOOL)keepOffset scrollToBottom:(BOOL)scrollToBottom; +- (void)prependHistoryMessages:(NSArray *)messages + openingMessage:(nullable KBAiChatMessage *)openingMessage; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m index e721216..da7551b 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m @@ -364,6 +364,39 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 scrollToBottom:(BOOL)scrollToBottom { CGFloat oldContentHeight = self.tableView.contentSize.height; CGFloat oldOffsetY = self.tableView.contentOffset.y; + KBAiChatMessage *anchorMessage = nil; + CGFloat anchorOffset = 0; + if (keepOffset) { + NSArray *visibleRows = self.tableView.indexPathsForVisibleRows; + if (visibleRows.count > 0) { + NSArray *sortedRows = [visibleRows sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *obj1, NSIndexPath *obj2) { + if (obj1.row < obj2.row) { + return NSOrderedAscending; + } else if (obj1.row > obj2.row) { + return NSOrderedDescending; + } + return NSOrderedSame; + }]; + NSIndexPath *anchorIndexPath = nil; + for (NSIndexPath *indexPath in sortedRows) { + if (indexPath.row < self.messages.count) { + KBAiChatMessage *message = self.messages[indexPath.row]; + if (message.type != KBAiChatMessageTypeTime) { + anchorIndexPath = indexPath; + break; + } + } + } + if (!anchorIndexPath) { + anchorIndexPath = sortedRows.firstObject; + } + if (anchorIndexPath && anchorIndexPath.row < self.messages.count) { + anchorMessage = self.messages[anchorIndexPath.row]; + CGRect anchorRect = [self.tableView rectForRowAtIndexPath:anchorIndexPath]; + anchorOffset = oldOffsetY - anchorRect.origin.y; + } + } + } [self.messages removeAllObjects]; if (messages.count > 0) { @@ -399,10 +432,18 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 NSLog(@"[KBChatTableView] ========== reloadWithMessages 结束 =========="); if (keepOffset) { - CGFloat newContentHeight = self.tableView.contentSize.height; - CGFloat delta = newContentHeight - oldContentHeight; - CGFloat offsetY = oldOffsetY + delta; - // 关键修复:使用非动画方式设置 offset,避免抖动 + CGFloat offsetY = oldOffsetY; + if (anchorMessage) { + NSInteger newIndex = [self.messages indexOfObjectIdenticalTo:anchorMessage]; + if (newIndex != NSNotFound) { + CGRect newRect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:newIndex inSection:0]]; + offsetY = newRect.origin.y + anchorOffset; + } + } else { + CGFloat newContentHeight = self.tableView.contentSize.height; + CGFloat delta = newContentHeight - oldContentHeight; + offsetY = oldOffsetY + delta; + } [self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO]; return; } @@ -439,6 +480,131 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 [self.messages addObject:message]; } +- (NSInteger)firstVisibleNonTimeRowExcludingMessage:(KBAiChatMessage *)excludedMessage { + NSArray *visible = self.tableView.indexPathsForVisibleRows; + if (visible.count == 0) { return NSNotFound; } + NSArray *sorted = [visible sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath * _Nonnull obj1, NSIndexPath * _Nonnull obj2) { + if (obj1.row < obj2.row) return NSOrderedAscending; + if (obj1.row > obj2.row) return NSOrderedDescending; + return NSOrderedSame; + }]; + for (NSIndexPath *ip in sorted) { + if (ip.row < self.messages.count) { + KBAiChatMessage *msg = self.messages[ip.row]; + if (msg.type != KBAiChatMessageTypeTime && msg != excludedMessage) { + return ip.row; + } + } + } + return sorted.firstObject.row; +} + +- (CGFloat)offsetForRow:(NSInteger)row { + if (row == NSNotFound) { return self.tableView.contentOffset.y; } + CGRect rect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:0]]; + return self.tableView.contentOffset.y - rect.origin.y; +} + +- (void)restoreOffsetWithMessage:(KBAiChatMessage *)message anchorOffset:(CGFloat)anchorOffset fallbackDelta:(CGFloat)fallbackDelta { + CGFloat offsetY = self.tableView.contentOffset.y + fallbackDelta; + if (message) { + NSInteger newIndex = [self.messages indexOfObjectIdenticalTo:message]; + if (newIndex != NSNotFound) { + CGRect newRect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:newIndex inSection:0]]; + offsetY = newRect.origin.y + anchorOffset; + } + } + [self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO]; +} + +- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message + inMessages:(NSArray *)messages { + if (messages.count == 0) { + return YES; + } + + KBAiChatMessage *lastMessage = nil; + for (NSInteger i = messages.count - 1; i >= 0; i--) { + KBAiChatMessage *msg = messages[i]; + if (msg.type != KBAiChatMessageTypeTime) { + lastMessage = msg; + break; + } + } + + if (!lastMessage) { + return YES; + } + + NSTimeInterval interval = [message.timestamp timeIntervalSinceDate:lastMessage.timestamp]; + if (interval >= kTimestampInterval) { + return YES; + } + + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDateComponents *lastComponents = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear + fromDate:lastMessage.timestamp]; + NSDateComponents *currentComponents = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear + fromDate:message.timestamp]; + return ![lastComponents isEqual:currentComponents]; +} + +- (NSArray *)messagesByInsertingTimestamps:(NSArray *)messages { + NSMutableArray *result = [NSMutableArray array]; + for (KBAiChatMessage *msg in messages) { + if ([self shouldInsertTimestampForMessage:msg inMessages:result]) { + KBAiChatMessage *timeMessage = [KBAiChatMessage timeMessageWithTimestamp:msg.timestamp]; + [result addObject:timeMessage]; + } + [result addObject:msg]; + } + return result; +} + +- (void)prependHistoryMessages:(NSArray *)messages + openingMessage:(nullable KBAiChatMessage *)openingMessage { + CGFloat oldContentHeight = self.tableView.contentSize.height; + NSInteger anchorRow = [self firstVisibleNonTimeRowExcludingMessage:openingMessage]; + KBAiChatMessage *anchorMsg = nil; + CGFloat anchorOffset = 0; + if (anchorRow != NSNotFound && anchorRow < self.messages.count) { + anchorMsg = self.messages[anchorRow]; + anchorOffset = [self offsetForRow:anchorRow]; + } + + NSArray *toInsert = [self messagesByInsertingTimestamps:messages]; + + [UIView performWithoutAnimation:^{ + if (toInsert.count > 0) { + NSInteger insertIndex = 0; + if (openingMessage) { + NSInteger openingIndex = [self.messages indexOfObjectIdenticalTo:openingMessage]; + if (openingIndex != NSNotFound) { + insertIndex = openingIndex + 1; + } + } + + NSRange range = NSMakeRange(insertIndex, toInsert.count); + NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:range]; + [self.messages insertObjects:toInsert atIndexes:set]; + + NSMutableArray *indexPaths = [NSMutableArray array]; + for (NSInteger i = 0; i < toInsert.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:insertIndex + i inSection:0]]; + } + + [self.tableView beginUpdates]; + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + [self.tableView endUpdates]; + } + + [self.tableView layoutIfNeeded]; + CGFloat newContentHeight = self.tableView.contentSize.height; + CGFloat delta = newContentHeight - oldContentHeight; + [self restoreOffsetWithMessage:anchorMsg anchorOffset:anchorOffset fallbackDelta:delta]; + }]; +} + /// 判断是否需要插入时间戳 - (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message { // 第一条消息总是显示时间 diff --git a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m index eba1b58..8d16fee 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m @@ -44,6 +44,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; +@property (nonatomic, assign) BOOL canTriggerLoadMore; + /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; @@ -94,6 +96,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe // 重置加载状态标志(但不清空 hasLoadedData) self.isLoading = NO; + self.canTriggerLoadMore = YES; // ✅ 移除了 self.hasLoadedData = NO; // 这样 Cell 复用时不会重复请求数据 @@ -174,6 +177,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe // 重置状态 self.isLoading = NO; + self.canTriggerLoadMore = YES; self.currentPage = 1; self.hasMoreHistory = YES; @@ -258,21 +262,25 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe pageNum:self.currentPage pageSize:20 completion:^(KBChatHistoryPageModel *pageModel, NSError *error) { - weakSelf.isLoading = NO; - - if (error) { - NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); - [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; - - // 如果是第一次加载失败,显示开场白 - if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) { - [weakSelf showOpeningMessage]; - } + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { return; } - weakSelf.hasLoadedData = YES; - weakSelf.hasMoreHistory = pageModel.hasMore; + if (error) { + NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); + dispatch_async(dispatch_get_main_queue(), ^{ + strongSelf.isLoading = NO; + [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; + if (strongSelf.currentPage == 1 && strongSelf.persona.introText.length > 0) { + [strongSelf showOpeningMessage]; + } + }); + return; + } + + strongSelf.hasLoadedData = YES; + strongSelf.hasMoreHistory = pageModel.hasMore; // 转换为 KBAiChatMessage NSMutableArray *newMessages = [NSMutableArray array]; @@ -296,56 +304,54 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe message.isComplete = YES; message.needsTypewriterEffect = NO; - [newMessages addObject:message]; +// [newMessages addObject:message]; + [newMessages insertObject:message atIndex:0]; } // 插入历史消息(确保开场白始终是第一条) // 关键修复:在 dispatch_async 之前保存当前页码,避免异步执行时 currentPage 已经被递增 - NSInteger loadedPage = weakSelf.currentPage; + NSInteger loadedPage = strongSelf.currentPage; if (loadedPage == 1) { // 第一页,直接赋值 - weakSelf.messages = newMessages; - [weakSelf ensureOpeningMessageAtTop]; + strongSelf.messages = newMessages; + [strongSelf ensureOpeningMessageAtTop]; } else { // 后续页,插入到开场白之后 - [weakSelf ensureOpeningMessageAtTop]; + [strongSelf ensureOpeningMessageAtTop]; if (newMessages.count > 0) { - NSUInteger insertIndex = [weakSelf hasOpeningMessageAtTop] ? 1 : 0; + NSUInteger insertIndex = [strongSelf hasOpeningMessageAtTop] ? 1 : 0; NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)]; - [weakSelf.messages insertObjects:newMessages atIndexes:indexSet]; + [strongSelf.messages insertObjects:newMessages atIndexes:indexSet]; } } // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - NSLog(@"[KBPersonaChatCell] ⚠️ strongSelf 为空,跳过刷新"); - return; + if (loadedPage == 1) { + NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: 0, scrollToBottom: 1", + (long)loadedPage); + [strongSelf.chatView reloadWithMessages:strongSelf.messages + keepOffset:NO + scrollToBottom:YES]; + } else { + NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, prependHistory", + (long)loadedPage); + KBAiChatMessage *openingMessage = [strongSelf hasOpeningMessageAtTop] ? strongSelf.messages.firstObject : nil; + [strongSelf.chatView prependHistoryMessages:newMessages openingMessage:openingMessage]; } - - // 使用保存的页码判断,而不是 currentPage(可能已被递增) - BOOL keepOffset = (loadedPage != 1); - BOOL scrollToBottom = (loadedPage == 1); - - NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: %d, scrollToBottom: %d", - (long)loadedPage, keepOffset, scrollToBottom); - - [strongSelf.chatView reloadWithMessages:strongSelf.messages - keepOffset:keepOffset - scrollToBottom:scrollToBottom]; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; // ✅ 保存到缓存(包含开场白) [[KBAIChatMessageCacheManager shared] saveMessages:strongSelf.messages forCompanionId:companionId]; + strongSelf.isLoading = NO; }); - weakSelf.currentPage++; + strongSelf.currentPage++; NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", - (long)weakSelf.currentPage - 1, + (long)strongSelf.currentPage - 1, (long)newMessages.count, pageModel.hasMore ? @"是" : @"否"); }]; @@ -357,7 +363,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe return; } - self.currentPage++; [self loadChatHistory]; } @@ -552,8 +557,11 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe CGFloat offsetY = scrollView.contentOffset.y; // 下拉到顶部,加载历史消息 - if (offsetY <= -50 && !self.isLoading) { + if (offsetY <= 50 && !self.isLoading && self.canTriggerLoadMore && self.hasMoreHistory) { + self.canTriggerLoadMore = NO; [self loadMoreHistory]; + } else if (offsetY > -20) { + self.canTriggerLoadMore = YES; } } diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index bea3ee4..a692cd0 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -225,7 +225,7 @@ }]; [self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.textInputTextView).offset(5); + make.left.equalTo(self.textInputTextView).offset(15); make.top.equalTo(self.textInputTextView).offset(8); }]; }