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

16 KiB
Raw Blame History

单角色应用迁移为多角色聊天架构

1. 文档目标

本文档定义 CozSweet 从单角色 Elio 迁移到多角色架构时,后端需要完成的接口、数据模型和兼容策略调整。

迁移目标:

  1. 后端提供统一角色目录。
  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. 角色目录接口

2.1 请求

GET <API_BASE_URL>/api/characters
Authorization: Bearer <TOKEN>

接口支持正式用户 Login Token 和游客 Guest Token。

2.2 成功响应

{
  "success": true,
  "message": "success",
  "data": {
    "items": [
      {
        "id": "character_elio",
        "slug": "elio",
        "displayName": "Elio Silvestri",
        "avatarUrl": "https://cdn.example.com/characters/elio/avatar.jpg"
      },
      {
        "id": "character_aria",
        "slug": "aria",
        "displayName": "Aria",
        "avatarUrl": "https://cdn.example.com/characters/aria/avatar.jpg"
      }
    ]
  }
}

公开字段只有:

字段 类型 是否必填 说明
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。
  • idslug 都需要数据库唯一约束。

角色目录不需要分页,也不返回 total

3. 聊天接口调整

3.1 通用规则

以下接口新增标准字段 characterId

POST /api/chat/send
GET  /api/chat/history
POST /api/chat/unlock-private
POST /api/chat/unlock-history

characterId 使用角色目录中的 id,例如 character_elio,不能传 slug 或显示名称。

后端必须使用以下维度定位聊天数据:

正式用户:userId + characterId
游客:guest identity/device identity + characterId

任何历史查询、消息发送、解锁和配额判断都不能只按用户查找。

3.2 发送消息

POST /api/chat/send
Content-Type: application/json
Authorization: Bearer <TOKEN>

现有请求体增加 characterId,其他字段保持原协议:

{
  "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 获取聊天历史

GET /api/chat/history?characterId=character_elio&limit=50&offset=0
Authorization: Bearer <TOKEN>
参数 类型 是否必填 说明
characterId string 角色业务 ID。
limit integer 保持现有分页规则。
offset integer 保持现有分页规则。

响应结构和分页字段保持不变。messagestotaloffset 必须只统计当前角色。

禁止在当前角色历史中返回其他角色的消息,即使消息属于同一用户。

3.4 解锁单条消息

POST /api/chat/unlock-private
Content-Type: application/json
Authorization: Bearer <TOKEN>

请求体增加 characterId

{
  "characterId": "character_elio",
  "messageId": "message-id",
  "lockType": "voice_message",
  "clientLockId": "client-generated-id"
}

现有 messageIdlockTypeclientLockId 规则保持不变。

后端必须验证:

  • 已存在消息的 character_id 与请求 characterId 一致。
  • 临时锁消息不存在 messageId 时,新建消息必须写入请求角色。
  • clientLockId 的幂等范围至少包含身份和角色,推荐唯一键:
identity + character_id + client_lock_id

跨角色解锁返回 CHARACTER_MISMATCH,不能扣积分,也不能修改消息。

3.5 一键解锁历史

POST /api/chat/unlock-history
Content-Type: application/json
Authorization: Bearer <TOKEN>

请求体由空请求调整为:

{
  "characterId": "character_elio"
}

后端只能统计和解锁当前角色的锁消息。requiredCredits、解锁数量和返回 messages 都必须按当前角色计算。

4. 角色最新消息接口

角色列表需要一次性获取各角色的最新消息,避免对每个角色分别请求 history。

4.1 请求

GET /api/chat/previews
Authorization: Bearer <TOKEN>

4.2 响应

{
  "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 获取相册

标准请求改为:

GET /api/private-room/albums?characterId=character_elio&limit=20
Authorization: Bearer <TOKEN>

兼容期继续接受旧参数:

GET /api/private-room/albums?character=elio&limit=20

参数优先级:

  1. 存在 characterId 时使用标准 ID。
  2. 兼容期仅存在 character 时,将旧 slug 解析为角色 ID。
  3. 两者同时存在但指向不同角色时返回 CHARACTER_MISMATCH
  4. 两者都缺失时,兼容期默认 Elio。

响应结构保持不变,不增加前端未使用的角色字段。

5.2 解锁相册

接口路径保持不变:

POST /api/private-room/albums/{albumId}/unlock

albumId 已能唯一确定角色,因此请求体不增加 characterId。后端必须根据 album 关联的角色进行权限、积分和解锁记录校验。

6. Tip 支付接口调整

6.1 创建订单

现有接口:

POST /api/payment/create-order

请求体增加可选字段 recipientCharacterId

{
  "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 更新时间。

enabledsort_order 是后端管理字段,不返回前端。

7.2 业务表

以下数据增加 character_id 外键:

  • conversations 或等价会话表;
  • chat messages
  • 锁消息、解锁记录和 clientLockId 幂等记录;
  • private albums 及相册解锁记录;
  • Tip 订单或 Tip 业务记录。

推荐索引:

(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_elioslug=elio
  2. 为相关业务表增加可空 character_id
  3. 将现有消息、锁记录、相册和 Tip 记录全部回填为 character_elio
  4. 检查不存在空角色或无法关联的数据。
  5. 创建联合索引和外键。
  6. 将必要业务表的 character_id 调整为非空。

回填前后需要核对每张表的记录数量,迁移不能删除历史聊天或解锁状态。

9. 兼容发布策略

第一阶段:后端兼容

  • 上线角色目录和新字段。
  • 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

{
  "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. 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_idVIP 和 Top-up 不保存。
  9. 角色预览接口一次返回各启用角色的最新消息,没有历史时返回 null
  10. 禁用角色从目录消失,直接访问业务接口返回 CHARACTER_DISABLED
  11. 缺少角色字段的旧请求在兼容期仍进入 Elio。
  12. 历史数据回填前后记录数量、积分和解锁状态一致。

13. OpenAPI 参考

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:
        - 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