Files
cozsweet-frontend-nextjs/docs/backend/FRONTEND_MULTI_CHARACTER_API.md
T

449 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 单角色应用迁移为多角色聊天架构
## 1. 文档目标
本文档定义 CozSweet 从单角色 Elio 迁移到多角色架构时,后端需要完成的接口、数据模型和兼容策略调整。
迁移目标:
1. 前端通过统一的 Character Catalog 边界读取角色目录,当前由本地只读目录提供,后端只识别稳定角色 ID。
2. 同一用户可以分别与多个角色聊天。
3. 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。
4. 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。
5. 现有客户端和历史数据继续默认归属于 Elio。
本文档中的标准角色标识:
| 类型 | Elio 示例 | 用途 |
| --- | --- | --- |
| `id` | `character_elio` | 数据库关联、API 请求和缓存隔离 |
| `slug` | `elio` | 前端 URL 和外部入口 |
后端业务接口必须使用 `id`,不能使用可能变化的展示名称。
环境地址:
| 环境 | API Base URL |
| --- | --- |
| test 测试环境 | `https://testapi.banlv-ai.com` |
| pro 预发环境 | `https://proapi.banlv-ai.com` |
| production 生产环境 | `https://api.banlv-ai.com` |
## 2. 前端 Character Catalog
后端角色目录协议尚未确定。过渡阶段由前端本地只读 Catalog 提供角色,并使用 `slug` 解析角色路径、使用 `id` 调用后端业务接口。业务页面只消费 Catalog 边界;后续切换为服务端目录时不改变角色路由和业务组件接口。
当前角色:
| `id` | `slug` | `displayName` | 头像 | 页面封面 | 私密空间 Banner |
| --- | --- | --- | --- | --- | --- |
| `character_elio` | `elio` | Elio Silvestri | `/images/avatar/elio.png` | `/images/cover/elio.png` | `/images/private-room/banner/elio.png` |
| `character_maya` | `maya` | Maya Tan | `/images/avatar/maya.png` | `/images/cover/maya.png` | `/images/private-room/banner/maya.png` |
| `character_nayeli` | `nayeli` | Nayeli Cervantes | `/images/avatar/nayeli.png` | `/images/cover/nayeli.png` | `/images/private-room/banner/nayeli.png` |
当前开发环境开放表中的全部角色。Catalog 同时保存短名称、页面资源、空聊天问候语、排序及 Chat、Private Room、Tip 能力。后端配置中的角色 ID 必须与前端目录一致。
## 3. 聊天接口调整
### 3.1 通用规则
以下接口新增标准字段 `characterId`
```text
POST /api/chat/send
GET /api/chat/history
POST /api/chat/unlock-private
POST /api/chat/unlock-history
```
`characterId` 使用前端本地角色目录中的 `id`,例如 `character_elio`,不能传 slug 或显示名称。
后端必须使用以下维度定位聊天数据:
```text
正式用户:userId + characterId
游客:guest identity/device identity + characterId
```
任何历史查询、消息发送、解锁和配额判断都不能只按用户查找。
### 3.2 发送消息
```http
POST /api/chat/send
Content-Type: application/json
Authorization: Bearer <TOKEN>
```
现有请求体增加 `characterId`,其他字段保持原协议:
```json
{
"characterId": "character_elio",
"message": "Hello Elio",
"image": "",
"imageId": "",
"imageThumbUrl": "",
"imageMediumUrl": "",
"imageOriginalUrl": "",
"imageWidth": 0,
"imageHeight": 0,
"useWebSocket": false
}
```
后端处理要求:
1. 校验角色存在且已启用。
2. 使用 `characterId` 选择对应角色 Prompt、模型配置和聊天上下文。
3. 只加载该用户与该角色的历史。
4. 新消息和生成结果必须写入相同 `characterId`
5. 响应继续沿用现有 `ChatSendResponse`,不额外返回前端不使用的角色字段。
如果后端启用 WebSocket,连接握手或每条发送帧也必须携带 `characterId`,不能依赖服务端进程内的“当前角色”状态。
### 3.3 获取聊天历史
```http
GET /api/chat/history?characterId=character_elio&limit=50&offset=0
Authorization: Bearer <TOKEN>
```
| 参数 | 类型 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| `characterId` | string | 是 | 角色业务 ID。 |
| `limit` | integer | 否 | 保持现有分页规则。 |
| `offset` | integer | 否 | 保持现有分页规则。 |
响应结构和分页字段保持不变。`messages``total``offset` 必须只统计当前角色。
禁止在当前角色历史中返回其他角色的消息,即使消息属于同一用户。
### 3.4 解锁单条消息
```http
POST /api/chat/unlock-private
Content-Type: application/json
Authorization: Bearer <TOKEN>
```
请求体增加 `characterId`
```json
{
"characterId": "character_elio",
"messageId": "message-id",
"lockType": "voice_message",
"clientLockId": "client-generated-id"
}
```
现有 `messageId``lockType``clientLockId` 规则保持不变。
后端必须验证:
- 已存在消息的 `character_id` 与请求 `characterId` 一致。
- 临时锁消息不存在 `messageId` 时,新建消息必须写入请求角色。
- `clientLockId` 的幂等范围至少包含身份和角色,推荐唯一键:
```text
identity + character_id + client_lock_id
```
跨角色解锁返回 `CHARACTER_MISMATCH`,不能扣积分,也不能修改消息。
### 3.5 一键解锁历史
```http
POST /api/chat/unlock-history
Content-Type: application/json
Authorization: Bearer <TOKEN>
```
请求体由空请求调整为:
```json
{
"characterId": "character_elio"
}
```
后端只能统计和解锁当前角色的锁消息。`requiredCredits`、解锁数量和返回 messages 都必须按当前角色计算。
## 4. 角色最新消息接口
角色列表需要一次性获取各角色的最新消息,避免对每个角色分别请求 history。
### 4.1 请求
```http
GET /api/chat/previews
Authorization: Bearer <TOKEN>
```
### 4.2 响应
```json
{
"success": true,
"message": "success",
"data": {
"items": [
{
"characterId": "character_elio",
"message": {
"id": "message-id",
"role": "assistant",
"type": "text",
"content": "How was your day?",
"createdAt": "2026-07-17T10:00:00Z"
}
},
{
"characterId": "character_aria",
"message": null
}
]
}
}
```
- `items` 按前端本地角色目录顺序返回当前可用角色。
- `message` 使用现有 ChatMessage wire 结构;没有历史时返回 `null`
- 锁消息继续遵循现有内容隐藏规则,不能通过预览接口泄露 URL 或锁定内容。
- 查询应批量完成,避免后端内部出现按角色逐条查询的 N+1 问题。
## 5. 私密空间接口调整
### 5.1 获取相册
标准请求改为:
```http
GET /api/private-room/albums?characterId=character_elio&limit=20
Authorization: Bearer <TOKEN>
```
兼容期继续接受旧参数:
```http
GET /api/private-room/albums?character=elio&limit=20
```
参数优先级:
1. 存在 `characterId` 时使用标准 ID。
2. 兼容期仅存在 `character` 时,将旧 slug 解析为角色 ID。
3. 两者同时存在但指向不同角色时返回 `CHARACTER_MISMATCH`
4. 两者都缺失时,兼容期默认 Elio。
响应结构保持不变,不增加前端未使用的角色字段。
### 5.2 解锁相册
接口路径保持不变:
```http
POST /api/private-room/albums/{albumId}/unlock
```
`albumId` 已能唯一确定角色,因此请求体不增加 `characterId`。后端必须根据 album 关联的角色进行权限、积分和解锁记录校验。
## 6. Tip 支付接口调整
### 6.1 创建订单
现有接口:
```http
POST /api/payment/create-order
```
请求体增加可选字段 `recipientCharacterId`
```json
{
"planId": "tip_coffee_medium",
"payChannel": "stripe",
"autoRenew": false,
"recipientCharacterId": "character_elio"
}
```
后端必须先根据 `planId` 判断订单类型:
| 订单类型 | `recipientCharacterId` 规则 |
| --- | --- |
| Tip | 必填,并校验角色存在且启用 |
| VIP | 忽略,不保存 |
| Top-up | 忽略,不保存 |
Tip 订单和最终支付记录需要保存 `recipient_character_id`,用于归属、统计和后续展示。
支付套餐、创建订单响应和订单状态响应不增加角色字段。
## 7. 数据模型调整
### 7.1 Characters 表
后台建议至少保存:
| 字段 | 说明 |
| --- | --- |
| `id` | 主键,稳定角色 ID。 |
| `slug` | 唯一 URL 标识。 |
| `display_name` | 展示名称。 |
| `avatar_url` | 头像地址。 |
| `enabled` | 是否对用户启用,仅后台使用。 |
| `sort_order` | 运营排序,仅后台使用。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
`enabled``sort_order` 是后端管理字段,不返回前端。
### 7.2 业务表
以下数据增加 `character_id` 外键:
- conversations 或等价会话表;
- chat messages
- 锁消息、解锁记录和 `clientLockId` 幂等记录;
- private albums 及相册解锁记录;
- Tip 订单或 Tip 业务记录。
推荐索引:
```text
(user_id, character_id, created_at)
(guest_id, character_id, created_at)
(character_id, enabled)
(identity, character_id, client_lock_id) UNIQUE
```
如果当前系统没有 conversations 表,可以继续使用 messages,但所有历史查询必须显式包含 `character_id`
## 8. 历史数据迁移
上线新接口前执行:
1. 创建 Elio 角色,固定 `id=character_elio``slug=elio`
2. 为相关业务表增加可空 `character_id`
3. 将现有消息、锁记录、相册和 Tip 记录全部回填为 `character_elio`
4. 检查不存在空角色或无法关联的数据。
5. 创建联合索引和外键。
6. 将必要业务表的 `character_id` 调整为非空。
回填前后需要核对每张表的记录数量,迁移不能删除历史聊天或解锁状态。
## 9. 兼容发布策略
### 第一阶段:后端兼容
- 上线角色 ID 配置和新字段。
- Chat 请求缺少 `characterId` 时暂时默认 `character_elio`
- Private Room 缺少角色时暂时默认 Elio。
- Tip 旧客户端缺少 recipient 时,旧 Tip 订单暂时默认 Elio。
- 记录缺少角色参数的调用次数和客户端版本。
### 第二阶段:前端迁移
- 新前端所有请求显式发送角色 ID。
- 动态路由和支付回跳携带角色上下文。
- 观察错误率、跨角色校验和旧请求占比。
### 第三阶段:收紧协议
- Chat 接口缺少 `characterId` 时返回 `CHARACTER_REQUIRED`
- Tip 订单缺少 recipient 时返回 `CHARACTER_REQUIRED`
- 删除 Private Room 旧 `character` 参数。
- 删除默认 Elio 的接口兼容逻辑。
由于 PWA 和浏览器可能长期缓存旧前端,第三阶段只能在旧请求流量降至可接受水平后执行。
只有“完全缺少角色字段”的旧请求可以在兼容期默认 Elio。显式传入不存在、禁用或不匹配角色时不得回退 Elio。
## 10. 错误响应
统一失败 envelope
```json
{
"success": false,
"message": "Character is not available",
"error": "CHARACTER_DISABLED"
}
```
新增错误:
| HTTP 状态 | error | 场景 |
| ---: | --- | --- |
| `400` | `CHARACTER_REQUIRED` | 收紧协议后缺少角色字段。 |
| `404` | `CHARACTER_NOT_FOUND` | `id` 或兼容 slug 不存在。 |
| `403` | `CHARACTER_DISABLED` | 角色存在但未启用。 |
| `409` | `CHARACTER_MISMATCH` | 消息、相册或重复参数属于不同角色。 |
角色错误不能扣积分、创建消息、创建订单或修改任何解锁状态。
## 11. 安全和一致性要求
- 角色 ID 必须由后端查询验证,不能信任客户端展示信息。
- 用户只能读取自己的某个角色会话,不能通过替换 characterId 读取其他用户数据。
- messageId、albumId 和 clientLockId 必须同时校验身份与角色归属。
- 角色禁用后不能继续发送、解锁、创建相册订单或 Tip 订单。
- 角色禁用不删除历史数据,恢复启用后历史仍可读取。
- 日志可以记录 `characterId`,但不能记录聊天正文、Token 或私密媒体 URL。
- Analytics 和业务统计统一使用角色 ID,不使用 displayName。
## 12. 联调验收
后端完成后至少验证:
1. 前端本地目录中的每个角色 ID 均可调用 Chat、Private Room 和 Tip 业务接口。
2. Login Token 和 Guest Token 均可按角色 ID 聊天。
3. 同一用户在 Elio 和其他角色中的历史完全隔离。
4. 发送消息、历史分页、单条解锁和历史解锁都限定当前角色。
5. 使用 Elio 的 messageId 配合其他角色 ID 解锁时返回 `CHARACTER_MISMATCH`,且不扣积分。
6. Private Room 的标准 characterId 与旧 character slug 在兼容期结果一致。
7. Tip 订单正确保存 `recipient_character_id`VIP 和 Top-up 不保存。
8. 角色预览接口一次返回各启用角色的最新消息,没有历史时返回 `null`
9. 后端禁用角色后,直接访问业务接口返回 `CHARACTER_DISABLED`;前端发布时同步移除本地记录。
10. 缺少角色字段的旧请求在兼容期仍进入 Elio。
11. 历史数据回填前后记录数量、积分和解锁状态一致。
## 13. OpenAPI 参考
```yaml
paths:
/api/chat/history:
get:
parameters:
- in: query
name: characterId
required: true
schema:
type: string
- in: query
name: limit
required: false
schema:
type: integer
- in: query
name: offset
required: false
schema:
type: integer
/api/chat/unlock-history:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [characterId]
properties:
characterId:
type: string
```