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

329 lines
10 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.
# CozSweet 多角色聊天权威协议
## 1. 状态与范围
本文是前端仓库中多角色目录、聊天、解锁、缓存隔离及关联业务的唯一人工维护协议。旧的单角色迁移方案和独立锁媒体协议已删除,不再作为实现或联调依据。
协议描述当前前端已经执行的行为,不描述尚未落地的后端迁移步骤。字段和路径由下列机器可验证入口约束:
| 边界 | 实现位置 |
| --- | --- |
| API 路径与方法 | `src/data/services/api/api_contract.json` |
| 请求与响应字段 | `src/data/schemas/character``src/data/schemas/chat` |
| 角色 ID、slug 与本地资源 | `src/data/constants/character.ts` |
| Chat UI 消息身份 | `src/stores/chat/ui-message.ts` |
| 本地缓存身份 | `src/data/repositories/chat_cache_identity.ts``src/lib/chat/chat_cache_keys.ts` |
如本文与上述实现发生差异,修改代码时必须在同一变更中更新本文,不能新增第二份并行协议。
## 2. 角色目录与路由
### 2.1 角色身份
`id` 是 API、状态机、缓存和数据归属使用的稳定业务身份;`slug` 只用于 URL。
| `id` | `slug` | 展示名称 |
| --- | --- | --- |
| `elio` | `elio` | Elio Silvestri |
| `maya-tan` | `maya` | Maya Tan |
| `nayeli-cervantes` | `nayeli` | Nayeli Cervantes |
不得向业务 API 发送 slug、展示名称、`@handle` 或旧 ID。旧 ID `character_elio``character_maya``character_nayeli` 仅用于本地持久化数据迁移。
### 2.2 角色目录接口
```http
GET <API_BASE_URL>/api/characters?capability=chat
Authorization: Bearer <TOKEN>
```
标准响应数据:
```json
{
"items": [
{
"id": "maya-tan",
"displayName": "Maya Tan",
"isActive": true,
"capabilities": {
"chat": true,
"privateContent": true
},
"sortOrder": 20
}
],
"defaultCharacterId": "elio"
}
```
前端只接纳同时满足以下条件的角色:
1. `id` 存在于本地资源目录;
2. `isActive=true`
3. `capabilities.chat=true`
服务端目录决定角色是否可聊天及排序;本地目录继续提供 slug、图片、文案和 Tip 能力。`privateRoom` 只有在本地能力和服务端 `privateContent` 同时开启时可用。
远端目录返回前,生产环境只使用 Elio 作为临时目录;非生产环境可使用完整本地目录。远端目录加载成功后,以合并结果为准。
### 2.3 页面路由
```text
/characters/{slug}/splash
/characters/{slug}/chat
/characters/{slug}/private-room
/characters/{slug}/tip
```
当前 Splash 是角色详情入口,不是全角色列表页。以下旧地址保留为默认角色重定向,并原样保留查询参数:
```text
/splash -> /characters/elio/splash
/chat -> /characters/elio/chat
/private-room -> /characters/elio/private-room
/tip -> /characters/elio/tip
```
Chat Provider 以 `characterId` 为边界,路由角色变化时创建新的 Actor。Private Room 的角色边界由 [Private Room 权威协议](./FRONTEND_PRIVATE_ROOM_API.md) 定义。
## 3. Chat HTTP 协议
### 3.1 发送消息
```http
POST <API_BASE_URL>/api/chat/send
Content-Type: application/json
Authorization: Bearer <TOKEN>
```
```json
{
"characterId": "maya-tan",
"message": "Hello Maya",
"useWebSocket": false
}
```
请求规则:
- `characterId` 必填;
- `message` 最大 4000 字符;
- `message``imageId``imageThumbUrl``imageMediumUrl``imageOriginalUrl` 至少一项有效;
- 图片请求可附带 `imageWidth``imageHeight`
- `useWebSocket` 缺失时按 `false` 处理。
前端使用的响应字段为:
```text
reply, audioUrl, messageId, isGuest, timestamp, image, lockDetail,
canSendMessage, creditBalance, creditsCharged, requiredCredits,
shortfallCredits
```
`messageId` 是后端消息身份,不是 React key。锁状态只读取 `lockDetail.locked``lockDetail.reason`
### 3.2 获取历史
```http
GET <API_BASE_URL>/api/chat/history?characterId=maya-tan&limit=50&offset=0
Authorization: Bearer <TOKEN>
```
| Query | 必填 | 规则 |
| --- | --- | --- |
| `characterId` | 是 | 角色业务 ID |
| `limit` | 否 | 前端默认 50 |
| `offset` | 否 | 前端默认 0 |
响应数据包含 `messages``total``limit``offset`,并可包含当前私密额度快照。消息字段:
```text
role, type, content, id, createdAt/created_at, audioUrl, image, lockDetail
```
前端兼容 `createdAt``created_at` 输入,解析后统一为 `createdAt`。缺失的可空载荷会由 Schema 归一化,业务组件不直接读取原始 envelope。
本地快照可先进入可渲染状态;网络历史随后覆盖缓存并更新 UI。网络同步期间新增的乐观消息按 `displayId` 保留。向上拉取更早页面时也按 `displayId` 去重。
### 3.3 最新消息预览
```http
GET <API_BASE_URL>/api/chat/previews
Authorization: Bearer <TOKEN>
```
```json
{
"items": [
{
"characterId": "elio",
"message": {
"id": "<MESSAGE_ID>",
"role": "assistant",
"type": "text",
"content": "How was your day?"
}
},
{
"characterId": "maya-tan",
"message": null
}
]
}
```
接口可用时,Splash 以批量结果更新各角色预览;接口失败时,当前角色可回退到 `history?limit=1&offset=0`。预览缓存仍使用角色会话身份隔离。
## 4. 解锁协议
### 4.1 解锁单条消息
```http
POST <API_BASE_URL>/api/chat/unlock-private
Content-Type: application/json
Authorization: Bearer <TOKEN>
```
已有后端消息:
```json
{
"characterId": "maya-tan",
"messageId": "<MESSAGE_ID>"
}
```
前端 Promotion 锁卡片:
```json
{
"characterId": "maya-tan",
"lockType": "voice_message",
"clientLockId": "<STABLE_CLIENT_LOCK_ID>"
}
```
请求必须包含 `characterId`,并至少包含 `messageId``lockType`。标准 `lockType` 为:
```text
voice_message
image_paywall
private_message
```
前端解析的响应字段:
```text
unlocked, content, messageId, clientLockId, lockType, audioUrl, image,
reason, creditBalance, creditsCharged, requiredCredits, shortfallCredits
```
处理规则:
1. `unlocked=true` 时更新内容、音频、图片和锁状态;
2. `unlocked=false` 时保持锁定,并根据 `reason` 决定是否打开支付流程;
3. `reason=not_found` 不打开支付流程;
4. 响应 `messageId` 只写入 `remoteId`,不得替换 `displayId`
5. Promotion 重试继续使用同一个 `clientLockId`
6. 跨支付、登录和图片 Overlay 恢复时同时保存显示身份与后端身份。
### 4.2 解锁当前角色历史
```http
POST <API_BASE_URL>/api/chat/unlock-history
Content-Type: application/json
Authorization: Bearer <TOKEN>
{"characterId":"maya-tan"}
```
请求和返回消息都限定当前角色。前端完成解锁后重新读取该角色历史,不复用其他角色 Actor 的消息。
### 4.3 角色错误
前端识别以下后端错误码:
| 错误码 | 前端行为 |
| --- | --- |
| `CHARACTER_DISABLED` | 禁止继续发送并刷新角色目录 |
| `CHARACTER_NOT_FOUND` | 清理待恢复导航、刷新目录并返回默认角色 |
| `CHARACTER_MISMATCH` | 不进入支付,刷新当前角色历史 |
| `CHARACTER_STATE_UNAVAILABLE` | 保留会话并提示稍后重试 |
任何角色错误都不得在前端回退为另一个角色的历史。
## 5. UI 消息身份协议
网络字段继续使用 `messageId`;前端 UI 严格区分三种身份:
| 字段 | 用途 | 是否稳定 |
| --- | --- | --- |
| `displayId` | React key、DOM 定位、Overlay URL、解锁目标 | 消息生命周期内必须稳定 |
| `remoteId` | 后端 `messageId`、媒体缓存、解锁 API | 后端返回后可补充 |
| `clientId` | 乐观发送消息或无后端 ID 的回复 | 创建时生成 |
标准 `displayId`
```text
历史消息 server:{encodedRemoteId}:{assistant|user}
同角色重复 ID server:{encodedRemoteId}:{role}:{occurrence}
无后端 ID 历史 legacy:{stableHash}
乐观文本 client:message:{uuid}
乐观图片 client:image:{uuid}
无 ID 回复 client:reply:{uuid}
错误提示 client:error:{uuid}
空会话问候语 greeting:{characterId}
Promotion promotion:{clientLockId}
```
同一个后端 ID 可能同时出现在 user 和 assistant 记录中,因此不得直接使用 `remoteId` 作为 React key。解锁前后 `displayId` 不变;图片 Overlay 始终写入 `displayId`,但读取时兼容旧的 `remoteId` URL。
## 6. 本地缓存与会话隔离
缓存 owner 规则:
```text
正式用户或已有用户 ID 的会话 user:{userId}
没有用户 ID 的 Guest 会话 device:{deviceId}
```
正式账号缺少持久化用户 ID 时跳过 Chat 缓存,不得回退到设备级或匿名缓存。
消息和媒体共同使用会话键:
```text
conversationKey = {ownerKey}::character:{encodeURIComponent(characterId)}
```
媒体键在该会话下继续包含后端消息 ID 和媒体类型:
```text
{conversationKey}:{remoteId}:{image|audio}
```
旧的无角色 owner key 迁移到 Elio;包含旧角色 ID 的 key 迁移到当前后端 ID。迁移是幂等的,不能覆盖已经存在的当前命名空间数据。
切换用户、Guest/正式账号或角色时,不得复用前一个 `conversationKey`。切换角色会销毁旧 Chat Actor;请求取消信号会阻止旧响应写入新角色状态。
## 7. 关联业务边界
- Private Room 的相册、解锁、Gallery 和支付回跳由 [Private Room 权威协议](./FRONTEND_PRIVATE_ROOM_API.md) 定义。
- Tip 的角色归属、订单轮询和支付回跳由 [Payment 权威协议](./FRONTEND_PAYMENT_API.md) 定义;Chat 只保存解锁所需的原角色回跳地址。
- 登录、支付和解锁回跳保存原角色动态 URL,不能降级为通用 `/chat`
- Analytics 可以使用 `characterId`,聊天正文不属于路由或身份协议的一部分。
## 8. 变更验收
协议相关变更至少验证:
1. Character、Chat、Storage、Navigation 和 Payment 的 TypeScript 类型检查;
2. `src/data/services/api/__tests__/multi_character_api.test.ts`
3. `src/stores/chat/__tests__`
4. `src/data/storage/chat/__tests__``src/data/storage/navigation/__tests__`
5. `src/app/chat/__tests__` 及 Chat 组件测试;
6. 同一用户的不同角色、Guest 与正式账号、账号切换后的历史和媒体均不串联;
7. 快速切换角色时,旧请求不能覆盖新 Actor;
8. 动态 URL、旧地址重定向、登录和支付回跳恢复正确角色;
9. 解锁后 `displayId` 不变,`remoteId` 可从响应补齐。