diff --git a/barrelsby.json b/barrelsby.json index 9d9d0e06..b59e69be 100644 --- a/barrelsby.json +++ b/barrelsby.json @@ -16,7 +16,6 @@ "./src/data/schemas/auth/request", "./src/data/schemas/auth/response", "./src/data/schemas/chat", - "./src/data/schemas/character", "./src/data/schemas/chat/request", "./src/data/schemas/chat/response", "./src/data/schemas/feedback", diff --git a/docs/backend/FRONTEND_MULTI_CHARACTER_API.md b/docs/backend/FRONTEND_MULTI_CHARACTER_API.md index 31e1cd0d..eb5ab557 100644 --- a/docs/backend/FRONTEND_MULTI_CHARACTER_API.md +++ b/docs/backend/FRONTEND_MULTI_CHARACTER_API.md @@ -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/characters -Authorization: Bearer -``` +当前角色: -接口支持正式用户 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 } ``` -- `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: diff --git a/docs/external-entry/README.md b/docs/external-entry/README.md index f81ee5e7..b4092a69 100644 --- a/docs/external-entry/README.md +++ b/docs/external-entry/README.md @@ -20,6 +20,7 @@ https:///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:///external-entry?target=chat ``` +进入 Maya 聊天: + +```text +https:///external-entry?target=chat&character=maya +``` + 携带 PSID 进入聊天,示例 PSID 为 `27511427698460020`: ```text @@ -74,6 +81,12 @@ https:///external-entry?target=tip https:///external-entry?target=private-room ``` +进入 Nayeli 私密空间: + +```text +https:///external-entry?target=private-room&character=nayeli +``` + `psid` 可以与任意 `target` 或聊天促销参数组合使用。参数值需要进行 URL 编码,不要在入口中传递登录 Token、Page Access Token 或 App Secret。 测试环境和正式环境的完整可点击示例见 [外部入口链接清单](./links.md)。 diff --git a/docs/external-entry/links.md b/docs/external-entry/links.md index a3759c1a..501795af 100644 --- a/docs/external-entry/links.md +++ b/docs/external-entry/links.md @@ -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`。 diff --git a/public/images/avatar/elio.png b/public/images/avatar/elio.png index 62107502..6b36e1fa 100644 Binary files a/public/images/avatar/elio.png and b/public/images/avatar/elio.png differ diff --git a/public/images/avatar/maya.png b/public/images/avatar/maya.png index 3048fc0a..8a33a533 100644 Binary files a/public/images/avatar/maya.png and b/public/images/avatar/maya.png differ diff --git a/public/images/avatar/nayeli.png b/public/images/avatar/nayeli.png index 5e9ef73e..1724d342 100644 Binary files a/public/images/avatar/nayeli.png and b/public/images/avatar/nayeli.png differ diff --git a/public/images/private-room/banner/elio.png b/public/images/private-room/banner/elio.png index dd57cde9..e98cf8dc 100644 Binary files a/public/images/private-room/banner/elio.png and b/public/images/private-room/banner/elio.png differ diff --git a/public/images/private-room/banner/maya.png b/public/images/private-room/banner/maya.png index cef692e0..d73b00c6 100644 Binary files a/public/images/private-room/banner/maya.png and b/public/images/private-room/banner/maya.png differ diff --git a/public/images/private-room/banner/nayeli.png b/public/images/private-room/banner/nayeli.png index a7ff0c19..f13eebdb 100644 Binary files a/public/images/private-room/banner/nayeli.png and b/public/images/private-room/banner/nayeli.png differ diff --git a/src/app/_components/core/app-bottom-nav.tsx b/src/app/_components/core/app-bottom-nav.tsx index cee62e8c..56a3d8d4 100644 --- a/src/app/_components/core/app-bottom-nav.tsx +++ b/src/app/_components/core/app-bottom-nav.tsx @@ -10,6 +10,7 @@ export type AppBottomNavVariant = "warm" | "dark"; export interface AppBottomNavProps { activeItem?: AppBottomNavItem | null; variant?: AppBottomNavVariant; + privateRoomLabel?: string; onChatClick: () => void; onPrivateRoomClick: () => void; } @@ -17,6 +18,7 @@ export interface AppBottomNavProps { export function AppBottomNav({ activeItem = null, variant = "warm", + privateRoomLabel = "Elio Private room", onChatClick, onPrivateRoomClick, }: AppBottomNavProps) { @@ -42,7 +44,7 @@ export function AppBottomNav({ onClick={onPrivateRoomClick} >