新增聊天记录

This commit is contained in:
2026-01-26 18:17:02 +08:00
parent f9d7579536
commit 6a177ceebc
14 changed files with 1447 additions and 18 deletions

View File

@@ -0,0 +1,43 @@
//
// KBChatHistoryModel.h
// keyBoard
//
// Created by Kiro on 2026/1/26.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 消息发送者类型
typedef NS_ENUM(NSInteger, KBChatSender) {
KBChatSenderUser = 0, // 用户
KBChatSenderAssistant = 1 // AI 助手
};
/// 聊天记录模型
@interface KBChatHistoryModel : NSObject
/// 消息 ID
@property (nonatomic, assign) NSInteger messageId;
/// 发送者0-用户1-AI
@property (nonatomic, assign) KBChatSender sender;
/// 消息内容
@property (nonatomic, copy) NSString *content;
/// 创建时间
@property (nonatomic, copy) NSString *createdAt;
#pragma mark - 扩展属性
/// 是否是用户消息
@property (nonatomic, assign, readonly) BOOL isUserMessage;
/// 是否是 AI 消息
@property (nonatomic, assign, readonly) BOOL isAssistantMessage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,43 @@
//
// KBChatHistoryModel.m
// keyBoard
//
// Created by Kiro on 2026/1/26.
//
#import "KBChatHistoryModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBChatHistoryModel
#pragma mark - MJExtension
//
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"messageId": @"id"
};
}
//
- (void)mj_keyValuesDidFinishConvertingToObject {
//
if (!self.content) {
self.content = @"";
}
if (!self.createdAt) {
self.createdAt = @"";
}
}
#pragma mark -
- (BOOL)isUserMessage {
return self.sender == KBChatSenderUser;
}
- (BOOL)isAssistantMessage {
return self.sender == KBChatSenderAssistant;
}
@end

View File

@@ -0,0 +1,45 @@
//
// KBChatHistoryPageModel.h
// keyBoard
//
// Created by Kiro on 2026/1/26.
//
#import <Foundation/Foundation.h>
#import "KBChatHistoryModel.h"
NS_ASSUME_NONNULL_BEGIN
/// 排序规则
@interface KBChatOrderRule : NSObject
@property (nonatomic, copy) NSString *column; // 排序字段
@property (nonatomic, assign) BOOL asc; // 是否升序
@end
/// 聊天记录分页数据模型
@interface KBChatHistoryPageModel : NSObject
/// 聊天记录列表
@property (nonatomic, strong) NSArray<KBChatHistoryModel *> *records;
/// 总记录数
@property (nonatomic, assign) NSInteger total;
/// 每页大小
@property (nonatomic, assign) NSInteger size;
/// 当前页码
@property (nonatomic, assign) NSInteger current;
/// 排序规则
@property (nonatomic, strong, nullable) NSArray<KBChatOrderRule *> *orders;
/// 总页数
@property (nonatomic, assign) NSInteger pages;
/// 是否还有更多数据
@property (nonatomic, assign, readonly) BOOL hasMore;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,39 @@
//
// KBChatHistoryPageModel.m
// keyBoard
//
// Created by Kiro on 2026/1/26.
//
#import "KBChatHistoryPageModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBChatOrderRule
@end
@implementation KBChatHistoryPageModel
#pragma mark - MJExtension
//
+ (NSDictionary *)mj_objectClassInArray {
return @{
@"records": [KBChatHistoryModel class],
@"orders": [KBChatOrderRule class]
};
}
//
- (void)mj_keyValuesDidFinishConvertingToObject {
if (!self.records) {
self.records = @[];
}
}
#pragma mark -
- (BOOL)hasMore {
return self.current < self.pages;
}
@end

View File

@@ -0,0 +1,408 @@
# 聊天记录 Model 说明
## 📦 Model 结构
### 1. KBChatHistoryModel聊天记录模型
```objc
@interface KBChatHistoryModel : NSObject
/// 消息 ID
@property (nonatomic, assign) NSInteger messageId;
/// 发送者0-用户1-AI
@property (nonatomic, assign) KBChatSender sender;
/// 消息内容
@property (nonatomic, copy) NSString *content;
/// 创建时间
@property (nonatomic, copy) NSString *createdAt;
/// 是否是用户消息
@property (nonatomic, assign, readonly) BOOL isUserMessage;
/// 是否是 AI 消息
@property (nonatomic, assign, readonly) BOOL isAssistantMessage;
@end
```
### 2. KBChatHistoryPageModel分页数据模型
```objc
@interface KBChatHistoryPageModel : NSObject
/// 聊天记录列表
@property (nonatomic, strong) NSArray<KBChatHistoryModel *> *records;
/// 总记录数
@property (nonatomic, assign) NSInteger total;
/// 每页大小
@property (nonatomic, assign) NSInteger size;
/// 当前页码
@property (nonatomic, assign) NSInteger current;
/// 总页数
@property (nonatomic, assign) NSInteger pages;
/// 是否还有更多数据
@property (nonatomic, assign, readonly) BOOL hasMore;
@end
```
---
## 🌐 接口说明
### 接口地址
```
POST /chat/history
```
### 请求参数
```json
{
"companionId": 0, // AI 陪聊角色 ID使用 KBPersonaModel.personaId
"pageNum": 1, // 页码
"pageSize": 20 // 每页大小
}
```
### 参数说明
- **companionId**:使用 `fetchPersonasWithPageNum` 接口返回的 `KBPersonaModel.personaId`
- **pageNum**:页码,从 1 开始
- **pageSize**:每页大小,建议 20
### 响应格式
```json
{
"code": 0,
"message": "success",
"data": {
"records": [
{
"id": 1,
"sender": 0, // 0-用户1-AI
"content": "你好",
"createdAt": "2026-01-26 10:00:00"
},
{
"id": 2,
"sender": 1,
"content": "你好!有什么可以帮助你的吗?",
"createdAt": "2026-01-26 10:00:05"
}
],
"total": 100,
"current": 1,
"pages": 5
}
}
```
---
## 📝 使用示例
### 1. 在 AiVM 中调用接口
```objc
#import "AiVM.h"
AiVM *aiVM = [[AiVM alloc] init];
[aiVM fetchChatHistoryWithCompanionId:123
pageNum:1
pageSize:20
completion:^(KBChatHistoryPageModel *pageModel, NSError *error) {
if (error) {
NSLog(@"加载失败:%@", error.localizedDescription);
return;
}
NSLog(@"加载成功:%ld 条消息", pageModel.records.count);
for (KBChatHistoryModel *message in pageModel.records) {
if (message.isUserMessage) {
NSLog(@"用户:%@", message.content);
} else {
NSLog(@"AI%@", message.content);
}
}
if (pageModel.hasMore) {
NSLog(@"还有更多数据");
}
}];
```
### 2. 在 VC 中使用
```objc
@interface YourViewController ()
@property (nonatomic, strong) AiVM *aiVM;
@property (nonatomic, strong) NSMutableArray<KBChatHistoryModel *> *messages;
@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, assign) BOOL hasMore;
@end
@implementation YourViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.aiVM = [[AiVM alloc] init];
self.messages = [NSMutableArray array];
self.currentPage = 1;
self.hasMore = YES;
[self loadChatHistory];
}
- (void)loadChatHistory {
NSInteger companionId = 123; // 当前人设 ID
__weak typeof(self) weakSelf = self;
[self.aiVM fetchChatHistoryWithCompanionId:companionId
pageNum:self.currentPage
pageSize:20
completion:^(KBChatHistoryPageModel *pageModel, NSError *error) {
if (error) {
NSLog(@"加载失败:%@", error.localizedDescription);
return;
}
// 追加数据
[weakSelf.messages addObjectsFromArray:pageModel.records];
weakSelf.hasMore = pageModel.hasMore;
// 刷新 UI
[weakSelf.tableView reloadData];
}];
}
- (void)loadMoreHistory {
if (!self.hasMore) {
return;
}
self.currentPage++;
[self loadChatHistory];
}
@end
```
### 3. 转换为 KBAiChatMessage
```objc
// 将 KBChatHistoryModel 转换为 KBAiChatMessage用于聊天界面展示
- (KBAiChatMessage *)convertToAiChatMessage:(KBChatHistoryModel *)historyModel {
if (historyModel.isUserMessage) {
return [KBAiChatMessage userMessageWithText:historyModel.content];
} else {
return [KBAiChatMessage assistantMessageWithText:historyModel.content];
}
}
// 批量转换
- (NSArray<KBAiChatMessage *> *)convertHistoryToMessages:(NSArray<KBChatHistoryModel *> *)history {
NSMutableArray *messages = [NSMutableArray array];
for (KBChatHistoryModel *item in history) {
[messages addObject:[self convertToAiChatMessage:item]];
}
return messages;
}
```
---
## 🔄 与 KBPersonaChatCell 集成
### 在 KBPersonaChatCell 中加载聊天记录
```objc
// KBPersonaChatCell.m
- (void)setPersona:(KBPersonaModel *)persona {
_persona = persona;
// 重置状态
self.hasLoadedData = NO;
self.isLoading = NO;
self.currentPage = 1;
self.hasMoreHistory = YES;
self.messages = [NSMutableArray array];
self.aiVM = [[AiVM alloc] init];
// 设置 UI
[self updateUI];
}
- (void)preloadDataIfNeeded {
if (self.hasLoadedData || self.isLoading) {
return;
}
[self loadChatHistory];
}
- (void)loadChatHistory {
if (self.isLoading || !self.hasMoreHistory) {
return;
}
self.isLoading = YES;
// 使用 persona.personaId 作为 companionId
NSInteger companionId = self.persona.personaId;
__weak typeof(self) weakSelf = self;
[self.aiVM fetchChatHistoryWithCompanionId:companionId
pageNum:self.currentPage
pageSize:20
completion:^(KBChatHistoryPageModel *pageModel, NSError *error) {
weakSelf.isLoading = NO;
if (error) {
NSLog(@"加载聊天记录失败:%@", error.localizedDescription);
return;
}
weakSelf.hasLoadedData = YES;
weakSelf.hasMoreHistory = pageModel.hasMore;
// 转换为 KBAiChatMessage
NSMutableArray *newMessages = [NSMutableArray array];
for (KBChatHistoryModel *item in pageModel.records) {
KBAiChatMessage *message;
if (item.isUserMessage) {
message = [KBAiChatMessage userMessageWithText:item.content];
} else {
message = [KBAiChatMessage assistantMessageWithText:item.content];
}
message.isComplete = YES;
[newMessages addObject:message];
}
// 插入到顶部(历史消息)
if (weakSelf.currentPage == 1) {
weakSelf.messages = newMessages;
} else {
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)];
[weakSelf.messages insertObjects:newMessages atIndexes:indexSet];
}
// 刷新 UI
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
}];
}
- (void)loadMoreHistory {
if (!self.hasMoreHistory || self.isLoading) {
return;
}
self.currentPage++;
[self loadChatHistory];
}
// 下拉加载更多
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
if (offsetY <= -50 && !self.isLoading) {
[self loadMoreHistory];
}
}
```
---
## 📊 数据流程
```
1. 用户滑动到某个人设
2. KBPersonaChatCell 触发预加载
3. 调用 AiVM.fetchChatHistoryWithCompanionId
4. 请求 POST /chat/history
5. 返回 KBChatHistoryPageModel
6. 转换为 KBAiChatMessage
7. 在 TableView 中展示
```
---
## 🎯 字段映射
### MJExtension 自动映射
| JSON 字段 | Model 属性 | 说明 |
|-----------|-----------|------|
| `id` | `messageId` | 消息 ID |
| `sender` | `sender` | 发送者0-用户1-AI |
| `content` | `content` | 消息内容 |
| `createdAt` | `createdAt` | 创建时间 |
### 枚举值说明
```objc
typedef NS_ENUM(NSInteger, KBChatSender) {
KBChatSenderUser = 0, // 用户
KBChatSenderAssistant = 1 // AI 助手
};
```
---
## ✅ 已完成功能
1. ✅ KBChatHistoryModel聊天记录模型
2. ✅ KBChatHistoryPageModel分页数据模型
3. ✅ AiVM 新增接口fetchChatHistoryWithCompanionId
4. ✅ MJExtension 配置JSON 自动转 Model
5. ✅ 扩展属性isUserMessage、isAssistantMessage、hasMore
6. ✅ 容错处理:字段为 null 时设置默认值
---
## 🚧 待实现功能
1. 在 KBPersonaChatCell 中集成聊天记录加载
2. 实现下拉加载更多历史消息
3. 实现消息时间戳显示
4. 实现消息发送功能
5. 实现消息缓存(避免重复请求)
---
## 📌 注意事项
1. **companionId**:直接使用 `KBPersonaModel.personaId`(人设列表接口返回的 ID
2. **时间格式**createdAt 是字符串,需要根据实际格式解析
3. **分页方向**:默认从最新消息开始加载,下拉加载历史消息
4. **消息顺序**:需要根据实际需求决定是正序还是倒序
5. **容错处理**:已做 null 值容错,确保不会崩溃
### 数据关联关系
```
KBPersonaModel (人设列表)
└─ personaId (人设 ID)
用作 companionId 参数
KBChatHistoryPageModel (聊天记录)
└─ records (消息列表)
```