426 lines
10 KiB
Markdown
426 lines
10 KiB
Markdown
# 聊天记录 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": 1, // 1-用户(右侧),2-AI(左侧)
|
||
"content": "你好",
|
||
"createdAt": "2026-01-26 10:00:00"
|
||
},
|
||
{
|
||
"id": 2,
|
||
"sender": 2,
|
||
"content": "你好!有什么可以帮助你的吗?",
|
||
"createdAt": "2026-01-26 10:00:05"
|
||
}
|
||
],
|
||
"total": 100,
|
||
"current": 1,
|
||
"pages": 5
|
||
}
|
||
}
|
||
```
|
||
|
||
### sender 字段说明
|
||
- **sender = 1**:用户消息(显示在右侧)
|
||
- **sender = 2**:AI 消息(显示在左侧)
|
||
|
||
---
|
||
|
||
## 📝 使用示例
|
||
|
||
### 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 = 1, // 用户(右侧显示)
|
||
KBChatSenderAssistant = 2 // AI 助手(左侧显示)
|
||
};
|
||
```
|
||
|
||
### 与 KBAiChatMessage 的映射关系
|
||
|
||
```objc
|
||
// KBChatHistoryModel → KBAiChatMessage
|
||
if (historyModel.sender == KBChatSenderUser) {
|
||
// sender = 1 → KBAiChatMessageTypeUser(右侧)
|
||
message = [KBAiChatMessage userMessageWithText:historyModel.content];
|
||
} else if (historyModel.sender == KBChatSenderAssistant) {
|
||
// sender = 2 → KBAiChatMessageTypeAssistant(左侧)
|
||
message = [KBAiChatMessage assistantMessageWithText:historyModel.content];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 已完成功能
|
||
|
||
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 (消息列表)
|
||
```
|