28 KiB
付费墙接口说明(PAYWALL_API)
适用服务:
ai-boyfriend-unified聊天接口路径:POST /api/chat/send历史接口路径:GET /api/chat/history当前无单条私密消息解锁接口;VIP 自动查看全部私密消息 WebSocket 路径:/ws字段命名:camelCase(与现有接口完全一致) 文档语言:中文
总览:三个付费墙功能
本文档覆盖三个会员限制功能,它们共用同一套「Elio 人设提示 + 是否展示开会员引导」的交互模式:
| 功能 | 触发场景 | 限制对象 | 核心接口 | 核心字段 |
|---|---|---|---|---|
| ① 每日免费消息次数 | 用户发消息 | 注册非VIP(游客豁免) | POST /api/chat/send |
blocked / blockReason / blockDetail |
| ② AI 男友照片查看 | 用户请求发图 | 非VIP(含游客) | POST /api/chat/send |
paywallTriggered / showUpgrade / imageType / imageUrl |
| ③ 私密消息锁定 | 查看被锁定的私密消息 | 非VIP(含游客) | GET /api/chat/history |
isPrivate / privateLocked / privateHint |
当前状态:付费墙、VIP 判定、订单接口、RabbitMQ 支付结果消费、VIP/DOL 发放逻辑已经写入后端。充值能否真正闭环取决于
PAYMENT_SERVICE_URL、RABBITMQ_URL、orders表迁移以及支付服务 B 是否部署完成。若 B 未配置或不可用,聊天付费墙仍会返回showUpgrade=true,但前端创建订单会失败,用户无法完成购买。
目录
- 设计原则与兼容性
- 功能①:免费消息次数限制
- 功能②:AI 男友照片查看
- 功能③:私密消息查看
- POST /api/chat/send 完整响应结构
- WebSocket 事件
- 支付服务与前端充值对接
- 前端接入清单
- 当前未生效或依赖外部配置的内容
- Elio 回复语音字段
- Manager 图片上传错误码
- 附录:图片意图关键词
1. 设计原则与兼容性
- 不新增聊天接口:功能①②都复用现有
POST /api/chat/send,功能③只复用GET /api/chat/history。 - 响应结构固定:付费墙相关字段在对应接口的每一条响应中都会出现(默认值
false/null),前端可稳定读取,不会"有时有有时没有"。 - 完全向后兼容:所有现有字段原样保留,仅新增字段,不删改任何旧字段。
- 升级引导判断:图片付费墙用
showUpgrade === true展示"开通会员"引导;消息次数限制沿用blocked === true+blockReason判断;私密消息只看privateLocked。 - Fail-safe:所有付费墙逻辑失败时降级放行,不影响正常聊天。
2. 功能①:免费消息次数限制
规则
- 生产环境:游客每天最多免费发送 30 条消息,游客累计最多 50 条消息;注册的非 VIP 用户每天最多免费发送 30 条消息。
- 预发环境:上述数量为生产的 1/10,即游客每天 3 条、累计 5 条;注册非 VIP 每天 3 条。
- VIP 不受限。
- 超限后,
POST /api/chat/send返回blocked=true,reply为空,前端据此弹开通会员提示。
触发时的响应(data 字段)
{
"mode": "http",
"reply": "",
"voiceUrl": "",
"audioUrl": "",
"intimacyChange": 0,
"newIntimacy": 0,
"relationshipStage": "密友",
"currentMood": "happy",
"messageId": "",
"isGuest": false,
"timestamp": 1780975180614,
"blocked": true,
"blockReason": "daily_limit",
"blockDetail": {
"type": "daily_msg_limit",
"usedToday": 30,
"limit": 30
},
"paywallTriggered": false,
"showUpgrade": false,
"imageType": null,
"imageUrl": null
}
前端处理
if (data.blocked === true && data.blockReason === "daily_limit") {
展示"今日免费消息已用完,开通会员畅聊"引导 UI;
data.blockDetail 提供 usedToday / limit 供文案展示。
}
if (data.blocked === true && data.blockReason === "total_limit") {
展示"游客免费消息已用完,注册或开通会员继续聊"引导 UI;
data.blockDetail 提供 usedTotal / limit 供文案展示。
}
注:消息次数限制当前用
blocked/blockReason/blockDetail表达(沿用既有字段)。这与showUpgrade不冲突——限流场景showUpgrade=false,前端通过blocked判断。
3. 功能②:AI 男友照片查看
规则
照片有两种触发方式:
(a) 用户主动索要(如"发张照片"、"让我看看你"、"send me a photo"):
- VIP → 返回当天最接近当前时间的 Elio 日程图(
imageUrl有值,imageType="elio_schedule")。 - 非VIP(含游客)→ 返回 Elio 升级提示,
showUpgrade=true、imageUrl=null。
(b) Elio 主动发图(无需用户索要):
- 普通聊天中,主生成模型判断"此刻适合发照片"(如夜晚、亲密氛围、用户在想念时),且用户为 VIP、当天主动照片配额未用完、当天确有日程图时,后端会在该条普通回复里附带
imageUrl(HTTP)/ 推送image事件(WS)。 - 非 VIP 静默不发、不打扰(不弹升级提示)。
- VIP 每天主动收图上限 3 张(
PHOTO_DAILY_LIMIT=3,UTC 日切)。 - 主动发图时
paywallTriggered=false、showUpgrade=false,前端只需照常渲染imageUrl即可。
对前端而言协议不变:无论 (a) 还是 (b),都通过同一个
imageUrl字段(HTTP)或image事件(WS)拿到图片,直接渲染即可。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
paywallTriggered |
bool | 本条是否触发图片付费墙 |
showUpgrade |
bool | 前端是否展示开会员引导(核心判断字段) |
imageType |
string | null | null=无图 / "elio_schedule"=Elio 日程图 |
imageUrl |
string | null | 图片公开 URL(VIP 有图时有值) |
三种场景(data 字段示例)
A. 非VIP 请求图片(当前所有用户):
{
"reply": "宝贝,我的照片只有会员才能看到哦……开通会员,我就把最近的照片发给你。😉",
"paywallTriggered": true,
"showUpgrade": true,
"imageType": null,
"imageUrl": null
}
B. VIP 请求图片 + 当天有图:
{
"reply": "嗯,你想看我……那就给你看一张。这是我今天下午的照片,你喜欢吗?",
"paywallTriggered": false,
"showUpgrade": false,
"imageType": "elio_schedule",
"imageUrl": "https://lehwkihwnlqkavhcspel.supabase.co/storage/v1/object/public/elio-schedules/schedules/42/a1b2c3d4.jpg"
}
C. VIP 请求图片 + 当天无图:
{
"reply": "今天还没拍什么好看的,等我有了好照片再发给你好不好?",
"paywallTriggered": false,
"showUpgrade": false,
"imageType": null,
"imageUrl": null
}
上面仅列出付费墙字段,完整 data 结构见第 5 节。
前端处理
1. 始终渲染 data.reply 为 Elio 文字气泡。
2. if (data.showUpgrade) 展示开会员引导。
3. if (data.imageUrl) 渲染图片(URL 可直接用于 <img src>,无需鉴权)。
4. 功能③:私密消息查看
规则
- AI 回复中若包含私密/亲密/大尺度内容,后端异步判定并给该消息打标
is_private=true,生成模糊预告private_hint。 - 用户拉取历史时:
- VIP → 所有历史私密消息直接返回完整内容;之前锁住的内容也会一起解开,后续也不再锁。
- 非VIP → 私密消息内容被屏蔽,返回
privateLocked=true+privateHint,前端展示锁定卡片。
- 当前产品没有单条私密消息解锁流程。是否展示完整内容,只取决于当前用户是不是 VIP。
4.1 GET /api/chat/history —— 历史含锁定标记
每条消息(data.messages[])字段:
| 字段 | 类型 | 说明 |
|---|---|---|
role |
string | user / assistant |
type |
string | text / voice;有 Elio 语音地址时为 voice |
content |
string | 消息内容;非VIP查看私密消息时为空字符串 |
id |
string | 消息 ID |
created_at |
string | ISO8601 时间 |
audioUrl |
string | null | Elio 语音播放 URL;用户文本消息为 null |
voiceUrl |
string | null | 同 audioUrl,兼容旧字段 |
isPrivate |
bool | null | 是否私密消息 |
privateLocked |
bool | null | true = 当前用户无权查看,内容已锁定 |
privateHint |
string | null | 非会员看到的模糊预告文案 |
data 顶层建议前端只关心:
| 字段 | 类型 | 说明 |
|---|---|---|
isVip |
bool | 当前用户是否 VIP |
兼容旧版本客户端,后端可能仍返回
privateFreeLimit、privateUsedToday、privateCanViewFree等旧字段;当前前端可以忽略。
历史响应示例(非VIP,含一条锁定的私密消息):
{
"messages": [
{
"role": "user",
"type": "text",
"content": "今天好想你",
"id": "msg-1",
"created_at": "2026-06-09T03:00:00Z",
"audioUrl": null,
"voiceUrl": null,
"isPrivate": false,
"privateLocked": false,
"privateHint": null
},
{
"role": "assistant",
"type": "voice",
"content": "",
"id": "msg-2",
"created_at": "2026-06-09T03:00:05Z",
"audioUrl": "https://.../audio/msg-2.mp3",
"voiceUrl": "https://.../audio/msg-2.mp3",
"isPrivate": true,
"privateLocked": true,
"privateHint": "他好像想说什么悄悄话,解锁会员才能看到…"
}
],
"total": 2,
"limit": 50,
"offset": 0,
"isVip": false
}
4.2 前端处理
拉取 history:
对每条 privateLocked===true 的消息,渲染为锁定卡片(展示 privateHint)。
用户开通 VIP 后:
重新拉取 history;
之前被锁住的私密消息会直接返回完整 content,不需要再调额外解锁接口。
5. POST /api/chat/send 完整响应结构
data 字段的全部字段(现有 + 新增),所有 /api/chat/send 响应都包含:
注意:
privateLocked/privateHint/isPrivate不属于/api/chat/send的响应字段;它们只出现在GET /api/chat/history的data.messages[]单条历史消息里。私密消息由后端异步打标,因此前端要在刷新/拉取历史时根据privateLocked=true渲染模糊锁定卡片。
| 字段 | 类型 | 现有/新增 | 说明 |
|---|---|---|---|
mode |
string | 现有 | "http" / "websocket" |
reply |
string | 现有 | Elio 回复文本 |
voiceUrl |
string | 现有 | 语音 URL(后台异步生成) |
audioUrl |
string | 现有 | 兼容旧客户端 |
intimacyChange |
int | 现有 | 亲密度变化(恒 0) |
newIntimacy |
int | 现有 | 当前亲密度 |
relationshipStage |
string | 现有 | 关系阶段 |
currentMood |
string | 现有 | Elio 情绪 |
messageId |
string | 现有 | 消息 ID |
isGuest |
bool | null | 现有 | 是否游客 |
timestamp |
int | 现有 | Unix 毫秒时间戳 |
blocked |
bool | null | 现有 | 是否消息次数限流拦截(功能①) |
blockReason |
string | null | 现有 | "daily_limit" / "total_limit" |
blockDetail |
object | null | 现有 | 每日限制:{ type, usedToday, limit };游客总量限制:{ type, usedTotal, limit } |
paywallTriggered |
bool | 新增 | 是否触发图片付费墙(功能②) |
showUpgrade |
bool | 新增 | 前端是否展示开会员引导 |
imageType |
string | null | 新增 | null / "elio_schedule" |
imageUrl |
string | null | 新增 | 图片公开 URL |
6. WebSocket 事件
通过 /ws 连接时:
image 事件(VIP 有图时额外推送)
{ "type": "image", "data": { "imageUrl": "https://.../schedules/42/xxx.jpg" } }
paywall_status 事件(每次图片请求都推送)
{
"type": "paywall_status",
"data": {
"paywallTriggered": true,
"showUpgrade": true,
"imageType": null,
"imageUrl": null
}
}
文字回复(含升级提示)沿用现有整句推送机制,前端无需改动。
7. 支付服务与前端充值对接
支付链路分三段:
前端 -> 后端 A(ai-boyfriend-unified) -> 支付服务 B -> RabbitMQ -> 后端 A 发放权益 -> 前端刷新状态
7.1 后端 A 提供给前端的接口
GET /api/payment/plans
获取套餐列表,无需登录。前端在开通会员/充值弹窗中展示。
响应 data.plans[] 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
plan_id |
string | 前端创建订单时传入的套餐 ID |
plan_name |
string | 展示名称 |
order_type |
string | 传给支付服务 B 的订单类型 |
amount_cents |
int | 金额,单位为分 |
original_amount_cents |
int | null | 原价/划线价,单位为分;仅 VIP 套餐返回 |
daily_price_cents |
int | null | 日均展示价,单位为分;仅 VIP 套餐返回 |
currency |
string | VIP 套餐当前为 USD;DOL 充值项当前为 CNY |
vip_days |
int | null | VIP 增加天数,null 表示永久或非 VIP 商品 |
dol_amount |
int | null | DOL 积分数量,VIP 商品为 null |
当前套餐:
| plan_id | plan_name | order_type | amount_cents | original_amount_cents | daily_price_cents | currency | 发放内容 |
|---|---|---|---|---|---|---|---|
vip_monthly |
Monthly | vip_monthly |
1990 | 2490 | 66 | USD | VIP +30 天 |
vip_quarterly |
Quarterly | vip_quarterly |
4990 | 5990 | 55 | USD | VIP +90 天 |
vip_annual |
Annual | vip_annual |
16990 | 19990 | 47 | USD | VIP +365 天 |
dol_100 |
100 DOL | dol |
600 | null | null | CNY | DOL +100 |
dol_500 |
500 DOL | dol |
2800 | null | null | CNY | DOL +500 |
POST /api/payment/create-order
创建订单,需登录。
POST /api/payment/create-order
Authorization: Bearer <user_token>
Content-Type: application/json
请求体:
{
"planId": "vip_monthly",
"payChannel": "stripe",
"autoRenew": true
}
字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
planId |
是 | 来自 GET /api/payment/plans 的 plan_id |
payChannel |
是 | 支付通道取值约定:stripe / ezpay / paycools。当前生产代码若仍只放开 stripe / ezpay,需要同步把 A/B 两侧白名单加上 paycools |
autoRenew |
是 | 是否自动续费;DOL 和永久会员建议传 false |
成功响应:
{
"success": true,
"data": {
"orderId": "pay_abc123",
"payParams": {
"checkout_url": "https://pay.example.com/checkout/pay_abc123"
}
}
}
payParams 是支付服务 B 返回给前端的拉起支付信息。对于 Stripe Checkout / ezPay / PayCools 这类浏览器跳转支付,B 必须在 payParams 内提供可直接打开的支付链接。PayCools 返回的支付链接应放在 checkout_url、payment_url 或 url 中。
当前 Stripe 返回为 Checkout 链接模式。前端创建订单成功后应优先打开:
data.paymentUrldata.checkoutUrldata.urldata.payParams.paymentUrldata.payParams.checkoutUrldata.payParams.url
打开支付链接后继续轮询 GET /api/payment/order-status?order_id=...;支付服务 B 收到 Stripe webhook 后会发布 MQ,后端 A 消费后把订单从 pending 更新为 paid。
前端读取支付链接的优先级:
payParams.checkout_urlpayParams.payment_urlpayParams.approval_urlpayParams.url
如果 B 使用 SDK 参数而不是跳转链接,也可以在 payParams 中返回 SDK 所需字段,但必须提前和前端约定字段名。后端 A 不解析 payParams,只做透传。
错误:
| 状态码 | 场景 |
|---|---|
400 |
planId 无效,或 payChannel 不是后端当前放开的支付通道 |
401 |
未登录或 token 无效 |
502 |
PAYMENT_SERVICE_URL 未配置,或支付服务 B 创建订单失败 |
500 |
后端 A 创建订单过程中发生未知错误 |
GET /api/payment/order-status
轮询订单状态,需登录。只能查询当前登录用户自己的订单。
GET /api/payment/order-status?order_id=pay_abc123
Authorization: Bearer <user_token>
响应:
{
"success": true,
"data": {
"orderId": "pay_abc123",
"status": "pending",
"orderType": "vip_monthly",
"planId": "vip_monthly"
}
}
status 取值:
| status | 前端动作 |
|---|---|
pending |
继续等待;建议每 3~5 秒轮询一次 |
paid |
停止轮询,刷新 VIP 状态或 DOL 余额 |
failed |
停止轮询,提示支付失败/取消 |
GET /api/payment/vip-status
查询当前用户 VIP 状态,需登录。
GET /api/payment/vip-status
Authorization: Bearer <user_token>
响应:
{
"success": true,
"data": {
"isVip": true,
"vipExpiresAt": "2026-07-17T10:00:00+00:00"
}
}
vipExpiresAt=null 表示永久会员。
7.2 后端 A 调支付服务 B:创建订单
后端 A 在收到前端 create-order 后,会调用支付服务 B:
POST {PAYMENT_SERVICE_URL}/orders
Authorization: Bearer {PAYMENT_SERVICE_API_KEY}
Content-Type: application/json
请求体:
{
"user_id": "用户 UUID",
"order_type": "vip_monthly",
"pay_channel": "paycools",
"automatic_renewal": true,
"amount_cents": 1990,
"currency": "USD"
}
B 必须返回:
{
"order_id": "pay_abc123",
"pay_params": {
"checkout_url": "https://pay.example.com/checkout/pay_abc123"
}
}
约定:
pay_params结构由支付服务 B 决定,后端 A 不解析,前端原样使用。- 浏览器跳转支付必须返回可打开的 URL,推荐统一使用
pay_params.checkout_url。 - PayCools 返回支付链接时,建议放在
pay_params.checkout_url;前端也兼容payment_url/approval_url/url。 - 当前支付对接文档约定的
pay_channel是stripe/ezpay/paycools;生产代码也需要同步支持这些取值。如果要接其他通道,需要先由支付服务 B 的/orders支持该通道,再把后端 A 的渠道校验和前端选项一起放开。 - 后端 A 会把
order_id、user_id、order_type、plan_id、status=pending写入本地orders表。 - 发放什么权益由 A 侧
plan_id决定,B 不需要知道 VIP 天数或 DOL 数量。
7.3 支付服务 B 回传支付结果:RabbitMQ
B 在支付成功或失败后,向 RabbitMQ 队列发布消息。
| 项目 | 值 |
|---|---|
| 队列名 | 默认 payment.events,可通过 RABBITMQ_QUEUE 修改 |
| 消息格式 | JSON UTF-8 |
| 推荐属性 | durable queue + persistent message |
消息体:
{
"user_id": "用户 UUID",
"order_id": "pay_abc123",
"status": "success",
"info": ""
}
字段:
| 字段 | 必填 | 说明 |
|---|---|---|
user_id |
是 | 当前订单所属用户 |
order_id |
是 | B 创建订单时返回给 A 的订单号 |
status |
是 | success / fail |
info |
否 | 失败原因或备注 |
sign |
视配置 | 如果 A 配置了 PAYMENT_WEBHOOK_SECRET,B 需要按双方约定签名 |
A 收到消息后的行为:
| status | A 侧处理 |
|---|---|
success |
查本地订单 -> 按 plan_id 发放 VIP 或 DOL -> 订单置为 paid -> 推送 payment_success |
fail |
订单置为 failed -> 推送 payment_failed |
幂等规则:同一订单已经是 paid 时,再收到 success 不会重复发放权益。
7.4 支付 WebSocket 事件
权益发放后,A 会通过 /ws 推送给在线用户。
VIP 支付成功:
{
"type": "payment_success",
"orderId": "pay_abc123",
"payType": "vip",
"planName": "月度会员",
"vipExpiresAt": "2026-07-17T10:00:00+00:00"
}
DOL 支付成功:
{
"type": "payment_success",
"orderId": "pay_def456",
"payType": "dol",
"planName": "100 DOL",
"dolAmount": 100,
"dolBalance": 350
}
支付失败:
{
"type": "payment_failed",
"orderId": "pay_abc123",
"info": "用户取消支付"
}
7.5 环境变量
PAYMENT_SERVICE_URL=https://pay.example.com
PAYMENT_SERVICE_API_KEY=<A 调 B 的鉴权 key,可空>
PAYMENT_SERVICE_TIMEOUT=10.0
RABBITMQ_URL=amqp://user:pass@host:5672/vhost
RABBITMQ_QUEUE=payment.events
PAYMENT_WEBHOOK_SECRET=<MQ 消息签名密钥,可空>
8. 前端接入清单
8.1 触发开通会员入口
前端需要在这些场景展示开通会员入口:
POST /api/chat/send返回blocked=true且blockReason="daily_limit":每日免费消息用完。POST /api/chat/send返回showUpgrade=true:照片付费墙或其他升级提示。GET /api/chat/history返回privateLocked=true:这条私密消息当前只有 VIP 可见。- 用户主动点击会员中心/充值按钮。
8.2 购买流程
推荐流程:
1. 打开升级弹窗
2. GET /api/payment/plans 加载套餐
3. 用户选择 planId、payChannel、autoRenew
4. POST /api/payment/create-order
5. 从 data.payParams 读取 checkout_url/payment_url/approval_url/url 并打开支付页,PayCools 链接也走这个逻辑,或按约定调用支付 SDK
6. 每 3~5 秒 GET /api/payment/order-status?order_id=...
7. 收到 paid 或 WebSocket payment_success 后停止轮询
8. payType=vip 时调用 /api/payment/vip-status 并刷新本地会员状态
9. payType=dol 时刷新用户资料或积分余额,使用最新 dolBalance
前端不应该自己推断权益是否发放成功,应以 order-status=paid、payment_success 或 vip-status 的后端结果为准。
前端也不应该调用任何“确认支付成功”接口来改订单状态;订单状态只能由支付服务 B 接到支付通道回调后,通过 RabbitMQ 通知后端 A 更新。
8.3 支付中状态
创建订单后到支付结果返回前,按钮/弹窗应显示支付处理中,避免重复创建订单。用户关闭支付窗口时不要立即判定失败,继续轮询一小段时间或允许用户手动刷新状态。
9. 当前未生效或依赖外部配置的内容
这些内容已经有后端代码或接口定义,但上线生效需要对应外部条件:
| 功能 | 代码状态 | 生效条件 | 未满足时表现 |
|---|---|---|---|
| 付费墙展示 | 已接入聊天、图片、私密消息 | 无额外条件 | 非 VIP 返回 blocked 或 showUpgrade |
| 创建订单 | 已有 /api/payment/create-order |
配置 PAYMENT_SERVICE_URL,支付服务 B 可访问,且 B 返回 order_id + 可拉起支付的 pay_params |
返回 502,或前端拿不到支付链接 |
| 支付结果发放权益 | 已有 RabbitMQ 消费者 | 配置 RABBITMQ_URL,B 发布 payment.events 消息 |
订单停留 pending,VIP/DOL 不会到账 |
| 订单轮询 | 已有 /api/payment/order-status |
执行 database/payment-migration.sql 创建 orders 表 |
查询不到订单或写入失败 |
| VIP 解锁付费功能 | 已有 VIP 判定 | users.is_vip=true 且未过期,或永久会员 |
非 VIP 仍按限制处理 |
| DOL 充值到账 | 已有 DOL 发放 | users.dol_balance 字段存在,B 回传成功 MQ |
前端不能只靠创建订单展示到账 |
| Elio 回复语音 | POST /api/chat/send 已返回 voiceUrl / audioUrl 字段;GET /api/chat/history 的消息项也返回 type / audioUrl;WS 模式会推 voice_ready |
TTS 服务可用,且前端按字段播放 | HTTP 首包里通常为空,后台生成后写入 DB;稍后拉 history 可拿到 audioUrl |
VIP 当前会解除/放宽的功能:
- 每日免费消息次数限制。
- 私密消息查看限制。
- AI 男友照片查看和主动发图。
- 语音/TTS 相关门控中需要 VIP 的普通语音回复。
10. Elio 回复语音字段
当前产品口径是 Elio 可以发语音给用户,用户不需要发语音给 Elio。因此聊天发送接口只需要前端传文字 message,不需要用户音频上传字段。
10.1 POST /api/chat/send 中的语音字段
POST /api/chat/send 的 data 中包含:
| 字段 | 说明 |
|---|---|
voiceUrl |
Elio 回复语音 URL,新字段 |
audioUrl |
兼容旧客户端,语义同 voiceUrl |
当前 HTTP 模式下,AI 回复文本会立即返回,TTS 语音在后台异步生成,所以首包里通常是:
{
"reply": "AI 的回复内容",
"voiceUrl": "",
"audioUrl": ""
}
后台生成完成后会把语音地址写入 chat_messages.voice_url。GET /api/chat/history 会把这条消息返回为 type="voice",并带 audioUrl / voiceUrl。因此前端若走 HTTP 首包,不能假设立刻拿到语音 URL;可以稍后刷新 history 获取。
语音消息对应的文字版不需要单独转写接口:
| 场景 | 前端直接读取的字段 |
|---|---|
POST /api/chat/send 首包 |
reply |
GET /api/chat/history 单条消息 |
content |
10.2 WebSocket 模式的语音事件
如果前端使用 WebSocket 模式,后端会在分句 TTS 生成完成后推送:
{
"type": "voice_ready",
"index": 0,
"voiceUrl": "https://.../audio/xxx.mp3",
"audioUrl": "https://.../audio/xxx.mp3"
}
前端应优先使用 voiceUrl 播放,audioUrl 仅作为旧字段兼容。
注意:当前
/api/chat/send请求体没有用户音频字段;用户侧语音输入不是当前需求,不需要额外转写字段或用户audioUrl。
11. Manager 图片上传错误码
用于 ai-boyfriend-Manager 后台 POST/PUT /api/schedule 的图片上传(运营管理员侧)。
11.1 HTTP 400 — 格式不支持
{ "detail": "不支持的文件格式 image/gif,仅允许 JPEG、PNG、WEBP" }
11.2 HTTP 400 — 文件过大(>10MB)
{ "detail": "文件大小超过 10 MB 限制(当前 12582912 字节)" }
11.3 image_warning — 图片处理失败(非阻断,HTTP 200)
图片通过校验但压缩/上传失败时不返回 500,而是 200 + image_warning,日程文字正常保存:
{ "id": 42, "title": "苏州行", "image_url": null, "image_warning": "图片压缩失败:PIL 解码错误" }
| image_warning 示例 | 含义 |
|---|---|
"图片压缩失败:PIL 解码错误" |
图片损坏或格式异常 |
"图片上传失败:Storage 连接超时" |
Storage 网络异常 |
"图片上传失败:权限不足" |
Bucket 权限配置错误 |
"数据库写入 image_url 失败" |
图片已传但 URL 未关联日程 |
12. 附录:图片意图关键词
正则匹配,仅检测消息前 600 字符,大小写不敏感:
| 语言 | 模式 |
|---|---|
| 中文 | 发.*照片、发.*图、让我看看你、给我看看你、你今天的照片、你最近的照片、你的照片、你的图片 |
| 英文 | send.*photo、send.*picture、show.*picture、let me see you、what do you look like |
附:数据库迁移
付费墙、支付、语音、图片功能依赖的库表迁移脚本:
database/paywall-migration.sql— users 会员字段(is_vip/vip_expires_at)、chat_messages 私密字段(is_private/private_hint)、daily_private_unlocks表database/payment-migration.sql—orders表,用于本地订单状态、轮询和 MQ 回调发放权益database/migrate_elio_schedules_image_url.sql— elio_schedules 表image_url列(功能②)database/voice-quota-migration.sql— users 语音配额字段(daily_voice_count/daily_voice_date)database/photo-quota-migration.sql— users 主动照片配额字段(daily_photo_count/daily_photo_date,Elio 主动发图日限)
上线前需在 Supabase SQL Editor 执行以上脚本,并创建
elio-schedules公开 Storage Bucket。 若支付功能要闭环,必须确认orders表、users.is_vip、users.vip_expires_at、users.dol_balance均已存在。