Files
cozsweet-frontend-nextjs/docs/backend/FRONTEND_MULTI_ROLE_CHAT_API.md
Codex 0357fbcaff
Docker Image / Build and Push Docker Image (push) Successful in 2m7s
refactor(private-zone): use canonical product name
2026-07-23 10:55:47 +08:00

10 KiB
Raw Permalink Blame History

CozSweet 多角色聊天权威协议

1. 状态与范围

本文是前端仓库中多角色目录、聊天、解锁、缓存隔离及关联业务的唯一人工维护协议。旧的单角色迁移方案和独立锁媒体协议已删除,不再作为实现或联调依据。

协议描述当前前端已经执行的行为,不描述尚未落地的后端迁移步骤。字段和路径由下列机器可验证入口约束:

边界 实现位置
API 路径与方法 src/data/services/api/api_contract.json
请求与响应字段 src/data/schemas/charactersrc/data/schemas/chat
角色 ID、slug 与本地资源 src/data/constants/character.ts
Chat UI 消息身份 src/stores/chat/ui-message.ts
本地缓存身份 src/data/repositories/chat_cache_identity.tssrc/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_eliocharacter_mayacharacter_nayeli 仅用于本地持久化数据迁移。

2.2 角色目录接口

GET <API_BASE_URL>/api/characters?capability=chat
Authorization: Bearer <TOKEN>

标准响应数据:

{
  "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 能力。privateZone 只有在本地能力和服务端 privateContent 同时开启时可用。

远端目录返回前,生产环境只使用 Elio 作为临时目录;非生产环境可使用完整本地目录。远端目录加载成功后,以合并结果为准。

2.3 页面路由

/characters/{slug}/splash
/characters/{slug}/chat
/characters/{slug}/private-zone
/characters/{slug}/tip

当前 Splash 是角色详情入口,不是全角色列表页。以下旧地址保留为默认角色重定向,并原样保留查询参数:

/splash       -> /characters/elio/splash
/chat         -> /characters/elio/chat
/private-zone -> /characters/elio/private-zone
/tip          -> /characters/elio/tip

Chat Provider 以 characterId 为边界,路由角色变化时创建新的 Actor。Private Zone 的角色边界由 Private Zone 权威协议 定义。

3. Chat HTTP 协议

3.1 发送消息

POST <API_BASE_URL>/api/chat/send
Content-Type: application/json
Authorization: Bearer <TOKEN>
{
  "characterId": "maya-tan",
  "message": "Hello Maya",
  "useWebSocket": false
}

请求规则:

  • characterId 必填;
  • message 最大 4000 字符;
  • messageimageIdimageThumbUrlimageMediumUrlimageOriginalUrl 至少一项有效;
  • 图片请求可附带 imageWidthimageHeight
  • useWebSocket 缺失时按 false 处理。

前端使用的响应字段为:

reply, audioUrl, messageId, isGuest, timestamp, image, lockDetail,
canSendMessage, creditBalance, creditsCharged, requiredCredits,
shortfallCredits

messageId 是后端消息身份,不是 React key。锁状态只读取 lockDetail.lockedlockDetail.reason

3.2 获取历史

GET <API_BASE_URL>/api/chat/history?characterId=maya-tan&limit=50&offset=0
Authorization: Bearer <TOKEN>
Query 必填 规则
characterId 角色业务 ID
limit 前端默认 50
offset 前端默认 0

响应数据包含 messagestotallimitoffset,并可包含当前私密额度快照。消息字段:

role, type, content, id, createdAt/created_at, audioUrl, image, lockDetail

前端兼容 createdAtcreated_at 输入,解析后统一为 createdAt。缺失的可空载荷会由 Schema 归一化,业务组件不直接读取原始 envelope。

本地快照可先进入可渲染状态;网络历史随后覆盖缓存并更新 UI。网络同步期间新增的乐观消息按 displayId 保留。向上拉取更早页面时也按 displayId 去重。

3.3 最新消息预览

GET <API_BASE_URL>/api/chat/previews
Authorization: Bearer <TOKEN>
{
  "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 解锁单条消息

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

已有后端消息:

{
  "characterId": "maya-tan",
  "messageId": "<MESSAGE_ID>"
}

前端 Promotion 锁卡片:

{
  "characterId": "maya-tan",
  "lockType": "voice_message",
  "clientLockId": "<STABLE_CLIENT_LOCK_ID>"
}

请求必须包含 characterId,并至少包含 messageIdlockType。标准 lockType 为:

voice_message
image_paywall
private_message

前端解析的响应字段:

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 解锁当前角色历史

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

历史消息         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 规则:

正式用户或已有用户 ID 的会话  user:{userId}
没有用户 ID 的 Guest 会话      device:{deviceId}

正式账号缺少持久化用户 ID 时跳过 Chat 缓存,不得回退到设备级或匿名缓存。

消息和媒体共同使用会话键:

conversationKey = {ownerKey}::character:{encodeURIComponent(characterId)}

媒体键在该会话下继续包含后端消息 ID 和媒体类型:

{conversationKey}:{remoteId}:{image|audio}

旧的无角色 owner key 迁移到 Elio;包含旧角色 ID 的 key 迁移到当前后端 ID。迁移是幂等的,不能覆盖已经存在的当前命名空间数据。

切换用户、Guest/正式账号或角色时,不得复用前一个 conversationKey。切换角色会销毁旧 Chat Actor;请求取消信号会阻止旧响应写入新角色状态。

7. 关联业务边界

  • Private Zone 的相册、解锁、Gallery 和支付回跳由 Private Zone 权威协议 定义。
  • Tip 的角色归属、订单轮询和支付回跳由 Payment 权威协议 定义;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 可从响应补齐。