# 前端锁消息解锁 API 接入文档 ## 2. 功能目标 前端可以先展示一条本地锁消息。用户点击解锁时,即使该消息还没有后端 `messageId`,后端也会创建对应的聊天记录,并根据余额决定是否生成真实内容。 核心规则: 1. 余额不足时不生成真实语音、图片或私密内容,不扣积分,但会保存锁消息并返回真实 `messageId`。 2. 用户充值后,前端使用同一个 `clientLockId` 再次解锁,后端更新原消息,不新增另一条聊天记录。 3. 成功扣积分并持久化解锁后,`history` 才返回解锁状态。 4. 刷新页面不会自动解锁。未扣积分、未消耗免费私密额度时,`history` 仍返回 `locked=true`。 5. 前端只能使用 `unlocked` 或 `lockDetail.locked` 判断锁状态,不能根据 URL 是否为空判断。 ## 4. 解锁接口 ### 4.1 请求地址 ```http POST /api/chat/unlock-private ``` 生产环境完整地址: ```http POST https://api.banlv-ai.com/api/chat/unlock-private ``` ### 4.2 请求参数 请求体为 JSON: ```json { "messageId": "可选,有就传", "lockType": "voice_message", "clientLockId": "前端生成的唯一ID,可选但强烈建议" } ``` | 字段 | 类型 | 是否必填 | 示例 | 说明 | | --- | --- | --- | --- | --- | | `messageId` | string | 否 | `"9a43..."` | 后端真实消息 ID。已有消息时传;前端临时锁消息首次解锁时可以不传。 | | `lockType` | string | 临时锁消息必填 | `"voice_message"` | 锁类型。没有有效 `messageId` 时必须传。 | | `clientLockId` | string | 否,强烈建议 | `"fb_voice_3737_001"` | 前端锁卡片唯一 ID,用于充值后重试、刷新找回和避免重复创建。 | `lockType` 支持以下值: | 标准值 | 兼容别名 | 积分价格 | | --- | --- | ---: | | `voice_message` | `voice`、`audio` | 20 | | `image_paywall` | `image`、`photo`、`picture` | 40 | | `private_message` | `private`、`private_topic`、`private_room` | 10 | 建议前端只使用标准值。 ### 4.3 `clientLockId` 生成规则 前端在创建锁卡片时生成一次,之后不得改变: ```ts const clientLockId = crypto.randomUUID(); ``` 同一条锁消息在以下场景中必须继续使用同一个值: - 第一次点击解锁; - 余额不足后进入充值; - 充值成功后重新点击解锁; - 网络超时后重试; - 前端重新渲染同一条本地锁卡片。 首次响应拿到后端 `messageId` 后,前端应同时保存 `messageId` 和 `clientLockId`。 ### 4.4 最小请求示例 解锁已有后端消息: ```bash curl -X POST 'https://api.banlv-ai.com/api/chat/unlock-private' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"messageId":""}' ``` 解锁前端临时语音锁消息: ```bash curl -X POST 'https://api.banlv-ai.com/api/chat/unlock-private' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "lockType":"voice_message", "clientLockId":"codex_test_voice_001" }' ``` 充值后建议把三个字段都传回: ```json { "messageId": "", "lockType": "voice_message", "clientLockId": "codex_test_voice_001" } ``` ## 5. 统一响应结构 ```json { "code": 200, "message": "success", "success": true, "data": {} } ``` 业务解锁失败通常也返回 HTTP 200。前端必须读取 `data.unlocked` 和 `data.reason`,不能只检查 HTTP 状态或顶层 `success`。 重要字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `unlocked` | boolean | 本次操作后是否已真实解锁。 | | `reason` | string/null | 本次操作结果原因。 | | `messageId` / `message_id` | string | 后端真实消息 ID。首次临时锁请求后也会返回。 | | `clientLockId` | string/null | 后端识别到的前端锁 ID。 | | `lockType` | string/null | 规范化后的锁类型。 | | `reply` / `response` | string | 当前消息文字。 | | `content` | string/null | 允许展示时返回内容;私密内容未解锁时可以为 `null`。 | | `type` / `messageType` | string | `text` 或 `voice`。 | | `audioUrl` / `audio_url` | string | 锁定语音为空字符串,成功解锁后是完整 URL。 | | `image.url` | string/null | 图片完整 URL;是否展示仍由 `lockDetail.locked` 决定。 | | `creditBalance` | number/null | 操作后的当前积分。 | | `creditsCharged` | number | 本次实际扣除积分;失败或免费额度解锁为 0。 | | `requiredCredits` | number | 当前锁类型需要的积分。 | | `shortfallCredits` | number | 余额不足时还差多少积分。 | | `lockDetail.locked` | boolean | 前端锁卡片的最终展示开关。 | | `lockDetail.showContent` | boolean | 是否允许展示文字内容。 | | `lockDetail.showUpgrade` | boolean | 是否展示充值入口。 | | `lockDetail.reason` | string/null | 锁定原因。 | ## 6. 余额不足 请求: ```json { "lockType": "voice_message", "clientLockId": "fb_voice_37370387172559600_001" } ``` 代表性响应: ```json { "code": 200, "message": "success", "success": true, "data": { "unlocked": false, "reason": "insufficient_credits", "type": "voice", "messageType": "voice", "messageId": "", "message_id": "", "clientLockId": "fb_voice_37370387172559600_001", "lockType": "voice_message", "reply": "I left a voice message for you. Unlock it to listen.", "content": "I left a voice message for you. Unlock it to listen.", "audioUrl": "", "audio_url": "", "creditBalance": 5, "creditsCharged": 0, "requiredCredits": 20, "shortfallCredits": 15, "lockDetail": { "locked": true, "showContent": true, "showUpgrade": true, "reason": "voice_message", "detail": { "messageId": "", "clientLockId": "fb_voice_37370387172559600_001", "lockType": "voice_message", "requiredCredits": 20 } } } } ``` 前端处理: 1. 使用返回的 `messageId` 替换本地临时消息 ID。 2. 保留原 `clientLockId`。 3. 保持锁定状态并打开充值页面。 4. 充值完成后再次调用解锁接口。 5. 请求期间禁用解锁按钮,避免用户连续点击。 ## 7. 解锁成功 ### 7.1 语音成功 后端先确认并扣除 20 积分,然后生成真实文字和语音,更新同一条聊天记录: ```json { "code": 200, "message": "success", "success": true, "data": { "unlocked": true, "reason": "ok", "type": "voice", "messageType": "voice", "messageId": "", "clientLockId": "fb_voice_37370387172559600_001", "lockType": "voice_message", "reply": "I made this voice just for you.", "content": "I made this voice just for you.", "audioUrl": "https://api.banlv-ai.com/audio/.mp3", "audio_url": "https://api.banlv-ai.com/audio/.mp3", "creditBalance": 10, "creditsCharged": 20, "requiredCredits": 20, "shortfallCredits": 0, "lockDetail": { "locked": false, "showContent": true, "showUpgrade": false } } } ``` 前端收到后直接用本次响应替换锁卡片,播放地址读取 `data.audioUrl`。 ### 7.2 图片成功 ```json { "unlocked": true, "reason": "ok", "messageId": "", "clientLockId": "fb_image_001", "lockType": "image_paywall", "image": { "type": "elio_schedule", "url": "https://dbapi.banlv-ai.com/storage/v1/object/public/" }, "creditBalance": 60, "creditsCharged": 40, "requiredCredits": 40, "lockDetail": { "locked": false, "showContent": true, "showUpgrade": false } } ``` 图片读取 `data.image.url`。后端已有图片锁消息可能在锁定时也返回真实 URL,因为 URL 本身不作为解锁凭证;前端仍必须根据 `lockDetail.locked` 遮挡或禁止查看。 前端临时创建、尚未确定图片内容的锁消息,在成功扣积分前可能没有图片 URL;成功后后端选择图片并返回 URL。 ### 7.3 私密文本成功 前端临时私密消息按 10 积分生成并解锁: ```json { "unlocked": true, "reason": "ok", "messageId": "", "clientLockId": "fb_private_001", "lockType": "private_message", "reply": "", "content": "", "creditsCharged": 10, "requiredCredits": 10, "lockDetail": { "locked": false, "showContent": true, "showUpgrade": false } } ``` 已有后端私密文本仍优先使用每日免费私密额度:登录用户默认每天 2 条,游客默认每天 1 条。成功消耗免费额度后也会持久解锁,但 `creditsCharged=0`。 ## 8. History 接口 ### 8.1 请求 ```http GET /api/chat/history?limit=50&offset=0 Authorization: Bearer ``` ```bash curl 'https://api.banlv-ai.com/api/chat/history?limit=50&offset=0' \ -H 'Authorization: Bearer ' ``` `limit=1&offset=0` 返回最新的一条数据库聊天记录展开后的消息;接口最终按聊天展示顺序返回 `data.messages`。 ### 8.2 锁定消息示例 ```json { "role": "assistant", "type": "voice", "content": "I left a voice message for you. Unlock it to listen.", "id": "", "created_at": "2026-07-13T08:00:00+00:00", "audioUrl": null, "image": { "type": null, "url": null }, "lockDetail": { "locked": true, "showContent": true, "showUpgrade": true, "reason": "voice_message", "detail": { "messageId": "", "clientLockId": "fb_voice_37370387172559600_001", "lockType": "voice_message", "requiredCredits": 20 } } } ``` ### 8.3 前端展示规则 ```ts const locked = message.lockDetail?.locked === true; const showText = message.lockDetail?.showContent !== false; if (locked) { // 展示锁卡片、遮罩或解锁按钮 } else { // 展示真实内容 } ``` 各类型规则: | 状态 | `lockDetail.locked` | URL/内容行为 | | --- | ---: | --- | | 锁定语音 | `true` | `audioUrl=null`,不能播放。 | | 已解锁语音 | `false` | `audioUrl` 是完整可播放 URL。 | | 锁定图片 | `true` | `image.url` 可能有值,也可能为空;必须遮挡。 | | 已解锁图片 | `false` | 使用完整 `image.url` 展示。 | | 锁定私密文本 | `true` | 根据 `showContent` 决定是否显示 `content`。 | | 已解锁私密文本 | `false` | 展示完整 `content`。 | ## 9. 推荐 TypeScript 类型 ```ts type LockType = "voice_message" | "image_paywall" | "private_message"; interface UnlockPrivateRequest { messageId?: string; lockType?: LockType; clientLockId?: string; } interface LockDetail { locked: boolean; showContent: boolean; showUpgrade: boolean; reason?: string | null; hint?: string | null; actionLabel?: string | null; detail?: { messageId?: string; clientLockId?: string; lockType?: LockType; requiredCredits?: number; refundedCredits?: number; } | null; } interface UnlockPrivateData { unlocked: boolean; reason?: string | null; messageId: string; message_id: string; clientLockId?: string | null; lockType?: LockType | null; type: "text" | "voice"; messageType: "text" | "voice"; reply: string; response: string; content?: string | null; audioUrl: string; audio_url: string; image: { type?: string | null; url?: string | null; }; creditBalance?: number | null; creditsCharged: number; requiredCredits: number; shortfallCredits: number; displayMessage: string; localizedMessage: string; lockDetail: LockDetail; } ``` ## 10. 异常与前端处理 | `data.reason` | 含义 | 前端处理 | | --- | --- | --- | | `ok` | 成功扣费并解锁 | 用响应替换锁卡片。 | | `not_locked` | 消息已经不是锁定状态 | 直接按已解锁内容展示,不再扣费。 | | `insufficient_credits` | 积分不足 | 保存返回的 `messageId`,保持锁定并进入充值。 | | `content_generation_failed` | 临时锁内容生成失败 | 后端尝试退回本次积分;保持锁定,允许稍后重试。 | | `voice_generation_failed` | 已有语音消息生成失败 | 未扣积分,保持锁定并允许重试。 | | `quota_exhausted` | 已有私密文本每日免费额度用完 | 展示 `displayMessage`。 | | `invalid_lock_type` | `lockType` 不支持 | 记录前端错误并停止请求。 | | `not_found` | 消息不存在且没有有效 `lockType` | 禁用当前解锁按钮或重新同步 history。 | 认证失败会返回 HTTP 401。请求字段类型不合法可能返回 HTTP 422。 ## 11. 前端完整流程 ```text 创建本地锁卡片并生成 clientLockId -> 用户点击解锁 -> POST /api/chat/unlock-private -> unlocked=true:替换卡片并展示真实内容 -> insufficient_credits:保存 messageId,打开充值 -> 充值成功 -> 使用相同 messageId + lockType + clientLockId 重试 -> unlocked=true:替换卡片 -> 页面刷新 -> GET /api/chat/history -> 只按 lockDetail.locked 恢复锁定或解锁状态 ```