2026-01-26 18:17:02 +08:00
|
|
|
|
# 聊天记录 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,
|
2026-01-26 20:36:51 +08:00
|
|
|
|
"sender": 1, // 1-用户(右侧),2-AI(左侧)
|
2026-01-26 18:17:02 +08:00
|
|
|
|
"content": "你好",
|
|
|
|
|
|
"createdAt": "2026-01-26 10:00:00"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 2,
|
2026-01-26 20:36:51 +08:00
|
|
|
|
"sender": 2,
|
2026-01-26 18:17:02 +08:00
|
|
|
|
"content": "你好!有什么可以帮助你的吗?",
|
|
|
|
|
|
"createdAt": "2026-01-26 10:00:05"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"total": 100,
|
|
|
|
|
|
"current": 1,
|
|
|
|
|
|
"pages": 5
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
### sender 字段说明
|
|
|
|
|
|
- **sender = 1**:用户消息(显示在右侧)
|
|
|
|
|
|
- **sender = 2**:AI 消息(显示在左侧)
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📝 使用示例
|
|
|
|
|
|
|
|
|
|
|
|
### 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) {
|
2026-01-26 20:36:51 +08:00
|
|
|
|
KBChatSenderUser = 1, // 用户(右侧显示)
|
|
|
|
|
|
KBChatSenderAssistant = 2 // AI 助手(左侧显示)
|
2026-01-26 18:17:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
### 与 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];
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ 已完成功能
|
|
|
|
|
|
|
|
|
|
|
|
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 (消息列表)
|
|
|
|
|
|
```
|