feat(characters): use local character catalog

This commit is contained in:
2026-07-17 16:03:18 +08:00
parent a210a98d98
commit b3ebd5cf3b
96 changed files with 1023 additions and 522 deletions
+24 -123
View File
@@ -6,7 +6,7 @@
迁移目标:
1. 后端提供统一角色目录
1. 前端维护统一的本地角色目录,后端只识别稳定角色 ID
2. 同一用户可以分别与多个角色聊天。
3. 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。
4. 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。
@@ -29,79 +29,19 @@
| pro 预发环境 | `https://proapi.banlv-ai.com` |
| production 生产环境 | `https://api.banlv-ai.com` |
## 2. 角色目录接口
## 2. 前端本地角色目录
### 2.1 请求
角色列表不通过网络接口加载。前端在本地维护只读目录,并使用 `slug` 解析角色路径、使用 `id` 调用后端业务接口。
```http
GET <API_BASE_URL>/api/characters
Authorization: Bearer <TOKEN>
```
当前角色:
接口支持正式用户 Login Token 和游客 Guest Token。
| `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` |
### 2.2 成功响应
```json
{
"success": true,
"message": "success",
"data": {
"items": [
{
"id": "character_elio",
"slug": "elio",
"displayName": "Elio Silvestri",
},
{
"id": "character_maya",
"slug": "maya",
"displayName": "Maya Tan",
},
{
"id": "character_nayeli",
"slug": "nayeli",
"displayName": "Nayeli Cervantes",
}
]
}
}
```
公开字段只有:
| 字段 | 类型 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string | 是 | 稳定、唯一、不可变的角色业务 ID。 |
| `slug` | string | 是 | 稳定、唯一、URL 安全的角色标识。 |
| `displayName` | string | 是 | 前端展示名称。 |
| `avatarUrl` | string | 是 | HTTPS 头像地址。 |
后端只返回启用角色,并按照后台运营顺序排列 `items`。前端直接使用数组顺序,不执行二次排序。
响应中不要增加以下字段:
- `enabled`
- `sortOrder`
- `coverUrl`
- `chatBackgroundUrl`
- `tagline`
- `emptyChatGreeting`
- `capabilities`
- 创建人、更新时间等后台管理字段
当前约定所有返回的角色都支持聊天、私密空间和打赏。
### 2.3 角色字段规则
- `id` 建议使用不可枚举 UUID 或稳定业务 ID,最大长度 64。
- `slug` 只允许小写字母、数字和连字符,建议校验 `^[a-z0-9]+(?:-[a-z0-9]+)*$`
- `slug` 发布后不能修改;必须修改时应保留旧 slug 重定向映射。
- `displayName` 去除首尾空白后不能为空,建议最大长度 80。
- `avatarUrl` 必须为有效 HTTPS URL。
- `id``slug` 都需要数据库唯一约束。
角色目录不需要分页,也不返回 `total`
本地记录同时保存短名称及 Splash、Private Room、Tip 使用的角色文案。后端不提供头像、封面、展示名称或角色目录接口,但后端配置中的角色 ID 必须与前端目录一致。
## 3. 聊天接口调整
@@ -116,7 +56,7 @@ POST /api/chat/unlock-private
POST /api/chat/unlock-history
```
`characterId` 使用角色目录中的 `id`,例如 `character_elio`,不能传 slug 或显示名称。
`characterId` 使用前端本地角色目录中的 `id`,例如 `character_elio`,不能传 slug 或显示名称。
后端必须使用以下维度定位聊天数据:
@@ -268,7 +208,7 @@ Authorization: Bearer <TOKEN>
}
```
- `items` 只包含当前启用角色,并保持角色目录顺序
- `items` 按前端本地角色目录顺序返回当前可用角色
- `message` 使用现有 ChatMessage wire 结构;没有历史时返回 `null`
- 锁消息继续遵循现有内容隐藏规则,不能通过预览接口泄露 URL 或锁定内容。
- 查询应批量完成,避免后端内部出现按角色逐条查询的 N+1 问题。
@@ -399,7 +339,7 @@ Tip 订单和最终支付记录需要保存 `recipient_character_id`,用于归
### 第一阶段:后端兼容
- 上线角色目录和新字段。
- 上线角色 ID 配置和新字段。
- Chat 请求缺少 `characterId` 时暂时默认 `character_elio`
- Private Room 缺少角色时暂时默认 Elio。
- Tip 旧客户端缺少 recipient 时,旧 Tip 订单暂时默认 Elio。
@@ -459,61 +399,22 @@ Tip 订单和最终支付记录需要保存 `recipient_character_id`,用于归
后端完成后至少验证:
1. `GET /api/characters` 只返回 4 个公开字段
2. 角色目录只包含启用角色,并严格按照后台排序
3. Login Token 和 Guest Token 均可获取角色目录并聊天
4. 同一用户在 Elio 和其他角色中的历史完全隔离
5. 发送消息、历史分页、单条解锁和历史解锁都限定当前角色
6. 使用 Elio 的 messageId 配合其他角色 ID 解锁时返回 `CHARACTER_MISMATCH`,且不扣积分
7. Private Room 的标准 characterId 与旧 character slug 在兼容期结果一致
8. Tip 订单正确保存 `recipient_character_id`VIP 和 Top-up 不保存
9. 角色预览接口一次返回各启用角色的最新消息,没有历史时返回 `null`
10. 禁用角色从目录消失,直接访问业务接口返回 `CHARACTER_DISABLED`
11. 缺少角色字段的旧请求在兼容期仍进入 Elio
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/characters:
get:
summary: List enabled characters in display order
security:
- bearerAuth: []
responses:
"200":
description: Enabled characters
content:
application/json:
schema:
type: object
required: [success, data]
properties:
success:
type: boolean
const: true
data:
type: object
required: [items]
properties:
items:
type: array
items:
type: object
required: [id, slug, displayName, avatarUrl]
additionalProperties: false
properties:
id:
type: string
slug:
type: string
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
displayName:
type: string
avatarUrl:
type: string
format: uri
/api/chat/history:
get:
parameters:
+13
View File
@@ -20,6 +20,7 @@ https://<APP_HOST>/external-entry?<参数>
| 参数 | 可选值或格式 | 用途 |
| --- | --- | --- |
| `target` | `chat``tip``private-room` | 指定进入的页面;不传或值无效时进入聊天页。 |
| `character` | `elio``maya``nayeli` | 指定角色;不传或值无效时使用 Elio。 |
| `psid` | string | 传入 Facebook Page-scoped User ID。 |
| `mode` | `promotion` | 开启聊天促销模式。 |
| `promotion_type` | `voice``image``private` | 指定促销消息类型,仅与 `mode=promotion` 一起使用。 |
@@ -38,6 +39,12 @@ PSID 保存、登录状态判断和 Facebook Identity 绑定逻辑见 [PSID 与
https://<APP_HOST>/external-entry?target=chat
```
进入 Maya 聊天:
```text
https://<APP_HOST>/external-entry?target=chat&character=maya
```
携带 PSID 进入聊天,示例 PSID 为 `27511427698460020`
```text
@@ -74,6 +81,12 @@ https://<APP_HOST>/external-entry?target=tip
https://<APP_HOST>/external-entry?target=private-room
```
进入 Nayeli 私密空间:
```text
https://<APP_HOST>/external-entry?target=private-room&character=nayeli
```
`psid` 可以与任意 `target` 或聊天促销参数组合使用。参数值需要进行 URL 编码,不要在入口中传递登录 Token、Page Access Token 或 App Secret。
测试环境和正式环境的完整可点击示例见 [外部入口链接清单](./links.md)。
+12
View File
@@ -9,23 +9,35 @@
| 入口 | 链接 |
| --- | --- |
| 普通聊天 | [打开普通聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat) |
| Maya 聊天 | [打开 Maya 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=maya) |
| Nayeli 聊天 | [打开 Nayeli 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=nayeli) |
| 携带 PSID 的聊天 | [打开 PSID 聊天示例](https://frontend-test.banlv-ai.com/external-entry?target=chat&psid=27511427698460020) |
| 语音促销 | [打开语音促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=voice) |
| 图片促销 | [打开图片促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=image) |
| 私密文本促销 | [打开私密文本促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=private) |
| 咖啡打赏 | [打开咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip) |
| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=maya) |
| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=nayeli) |
| 私密空间 | [打开私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-room) |
| Maya 私密空间 | [打开 Maya 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=maya) |
| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=nayeli) |
## 正式环境
| 入口 | 链接 |
| --- | --- |
| 普通聊天 | [打开普通聊天](https://cozsweet.com/external-entry?target=chat) |
| Maya 聊天 | [打开 Maya 聊天](https://cozsweet.com/external-entry?target=chat&character=maya) |
| Nayeli 聊天 | [打开 Nayeli 聊天](https://cozsweet.com/external-entry?target=chat&character=nayeli) |
| 携带 PSID 的聊天 | [打开 PSID 聊天示例](https://cozsweet.com/external-entry?target=chat&psid=27511427698460020) |
| 语音促销 | [打开语音促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=voice) |
| 图片促销 | [打开图片促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=image) |
| 私密文本促销 | [打开私密文本促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=private) |
| 咖啡打赏 | [打开咖啡打赏](https://cozsweet.com/external-entry?target=tip) |
| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=maya) |
| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=nayeli) |
| 私密空间 | [打开私密空间](https://cozsweet.com/external-entry?target=private-room) |
| Maya 私密空间 | [打开 Maya 私密空间](https://cozsweet.com/external-entry?target=private-room&character=maya) |
| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://cozsweet.com/external-entry?target=private-room&character=nayeli) |
如需在其他入口携带 PSID,在链接末尾追加 `&psid=27511427698460020`