Files
cozsweet-frontend-nextjs/PAYWALL_API.md
T

29 KiB
Raw Blame History

付费墙接口说明(PAYWALL_API

适用服务:ai-boyfriend-unified 聊天接口路径:POST /api/chat/send 历史接口路径:GET /api/chat/history 私密解锁接口:POST /api/chat/unlock-private 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 + POST /api/chat/unlock-private isPrivate / privateLocked / privateHint / showUpgrade

当前状态:付费墙、VIP 判定、订单接口、RabbitMQ 支付结果消费、VIP/DOL 发放逻辑已经写入后端。充值能否真正闭环取决于 PAYMENT_SERVICE_URLRABBITMQ_URLorders 表迁移以及支付服务 B 是否部署完成。若 B 未配置或不可用,聊天付费墙仍会返回 showUpgrade=true,但前端创建订单会失败,用户无法完成购买。


目录

  1. 设计原则与兼容性
  2. 功能①:每日免费消息次数
  3. 功能②:AI 男友照片查看
  4. 功能③:私密消息查看
  5. POST /api/chat/send 完整响应结构
  6. WebSocket 事件
  7. 支付服务与前端充值对接
  8. 前端接入清单
  9. 当前未生效或依赖外部配置的内容
  10. 通话与语音补充
  11. Manager 图片上传错误码
  12. 附录:图片意图关键词

1. 设计原则与兼容性

  1. 不新增聊天接口:功能①②都复用现有 POST /api/chat/send,功能③复用 GET /api/chat/history 并新增一个 POST /api/chat/unlock-private 解锁接口。
  2. 响应结构固定:付费墙相关字段在对应接口的每一条响应中都会出现(默认值 false/null),前端可稳定读取,不会"有时有有时没有"。
  3. 完全向后兼容:所有现有字段原样保留,仅新增字段,不删改任何旧字段。
  4. 统一的展示判断字段 showUpgrade:前端只需判断 showUpgrade === true 即展示"开通会员"引导,无需关心三个功能各自的内部逻辑。
  5. Fail-safe:所有付费墙逻辑失败时降级放行,不影响正常聊天。

2. 功能①:每日免费消息次数

规则

  • 注册的非VIP用户每天最多免费发送 30 条消息(FREE_DAILY_MSG_LIMIT=30,按 UTC 日切)。
  • 游客不受限VIP 不受限
  • 超限后,POST /api/chat/send 返回 blocked=truereply 为空,前端据此弹开通会员提示。

触发时的响应(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 供文案展示。
}

注:每日限流当前用 blocked/blockReason/blockDetail 表达(沿用既有字段)。这与 showUpgrade 不冲突——限流场景 showUpgrade=false,前端通过 blocked 判断。


3. 功能②:AI 男友照片查看

规则

照片有两种触发方式:

(a) 用户主动索要(如"发张照片"、"让我看看你"、"send me a photo"):

  • VIP → 返回当天最接近当前时间的 Elio 日程图(imageUrl 有值,imageType="elio_schedule")。
  • 非VIP(含游客)→ 返回 Elio 升级提示,showUpgrade=trueimageUrl=null

(b) Elio 主动发图(无需用户索要):

  • 普通聊天中,主生成模型判断"此刻适合发照片"(如夜晚、亲密氛围、用户在想念时),且用户为 VIP、当天主动照片配额未用完、当天确有日程图时,后端会在该条普通回复里附带 imageUrlHTTP/ 推送 image 事件(WS)。
  • 非 VIP 静默不发、不打扰(不弹升级提示)。
  • VIP 每天主动收图上限 3 张PHOTO_DAILY_LIMIT=3UTC 日切)。
  • 主动发图时 paywallTriggered=falseshowUpgrade=false,前端只需照常渲染 imageUrl 即可。

对前端而言协议不变:无论 (a) 还是 (b),都通过同一个 imageUrl 字段(HTTP)或 image 事件(WS)拿到图片,直接渲染即可。

响应字段

字段 类型 说明
paywallTriggered bool 本条是否触发图片付费墙
showUpgrade bool 前端是否展示开会员引导(核心判断字段)
imageType string | null null=无图 / "elio_schedule"=Elio 日程图
imageUrl string | null 图片公开 URLVIP 有图时有值)

三种场景(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 每天有 1 次免费解锁额度(FREE_DAILY_PRIVATE_VIEWS=1UTC 日切)。
  • 解锁通过 POST /api/chat/unlock-private 完成;免费额度用尽则 showUpgrade=true,前端弹开通会员提示。

4.1 GET /api/chat/history —— 历史含锁定标记

每条消息(data.messages[])字段:

字段 类型 说明
role string user / assistant
content string 消息内容;私密且未解锁时为空字符串
id string 消息 ID(解锁时作为 messageId 传入)
created_at string ISO8601 时间
isPrivate bool | null 是否私密消息
privateLocked bool | null true = 当前用户无权查看,内容已锁定
privateHint string | null 非会员看到的模糊预告文案

data 顶层新增的配额状态字段:

字段 类型 说明
isVip bool 当前用户是否 VIP
privateFreeLimit int 每日免费查看上限(当前为 1
privateUsedToday int 今日已用次数
privateCanViewFree bool 今日是否还有免费额度(VIP 恒为 true)

历史响应示例(非VIP,含一条锁定的私密消息):

{
  "messages": [
    {
      "role": "user",
      "content": "今天好想你",
      "id": "msg-1",
      "created_at": "2026-06-09T03:00:00Z",
      "isPrivate": false,
      "privateLocked": false,
      "privateHint": null
    },
    {
      "role": "assistant",
      "content": "",
      "id": "msg-2",
      "created_at": "2026-06-09T03:00:05Z",
      "isPrivate": true,
      "privateLocked": true,
      "privateHint": "他好像想说什么悄悄话,解锁会员才能看到…"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0,
  "isVip": false,
  "privateFreeLimit": 1,
  "privateUsedToday": 0,
  "privateCanViewFree": true
}

4.2 POST /api/chat/unlock-private —— 解锁私密消息

前端在用户点击锁定卡片时调用。当前前端可暂不调用,接口已就绪。

请求体:

{ "messageId": "msg-2" }

响应 data 字段:

字段 类型 说明
unlocked bool true = 已解锁,可显示 content
content string | null 解锁成功时返回完整私密内容,否则 null
showUpgrade bool true = 免费次数用尽,前端弹开通会员提示
paywallTriggered bool 同 showUpgrade,业务语义标识
privateFreeLimit int 每日免费查看上限
privateUsedToday int 今日已用次数
reason string ok / quota_exceeded / not_private / not_found

场景 1 — 非VIP 当日仍有免费额度(解锁成功,消耗 1 次):

{
  "unlocked": true,
  "content": "(完整的私密回复内容…)",
  "showUpgrade": false,
  "paywallTriggered": false,
  "privateFreeLimit": 1,
  "privateUsedToday": 1,
  "reason": "ok"
}

场景 2 — 非VIP 当日免费额度已用尽(需开会员):

{
  "unlocked": false,
  "content": null,
  "showUpgrade": true,
  "paywallTriggered": true,
  "privateFreeLimit": 1,
  "privateUsedToday": 1,
  "reason": "quota_exceeded"
}

场景 3 — VIP 用户(直接解锁,不消耗额度):

{
  "unlocked": true,
  "content": "(完整的私密回复内容…)",
  "showUpgrade": false,
  "paywallTriggered": false,
  "privateFreeLimit": 1,
  "privateUsedToday": 0,
  "reason": "ok"
}

场景 4 — 消息不存在 / 非私密:

{ "unlocked": false, "content": null, "showUpgrade": false, "reason": "not_found" }
{ "unlocked": true, "content": "(普通内容)", "showUpgrade": false, "reason": "not_private" }

前端处理

拉取 history
  对每条 privateLocked===true 的消息,渲染为锁定卡片(展示 privateHint)。

用户点击锁定卡片 → 调 POST /api/chat/unlock-private { messageId }
  if (resp.unlocked === true) 用 resp.content 替换卡片,显示完整内容;
  else if (resp.showUpgrade === true) 弹"开通会员查看更多私密消息"提示。

5. POST /api/chat/send 完整响应结构

data 字段的全部字段(现有 + 新增),所有 /api/chat/send 响应都包含:

注意:privateLocked / privateHint / isPrivate 不属于 /api/chat/send 的响应字段;它们只出现在 GET /api/chat/historydata.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 套餐当前为 USDDOL 充值项当前为 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/plansplan_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_urlpayment_urlurl 中。

当前 Stripe 返回为 Checkout 链接模式。前端创建订单成功后应优先打开:

  1. data.paymentUrl
  2. data.checkoutUrl
  3. data.url
  4. data.payParams.paymentUrl
  5. data.payParams.checkoutUrl
  6. data.payParams.url

打开支付链接后继续轮询 GET /api/payment/order-status?order_id=...;支付服务 B 收到 Stripe webhook 后会发布 MQ,后端 A 消费后把订单从 pending 更新为 paid

前端读取支付链接的优先级:

  1. payParams.checkout_url
  2. payParams.payment_url
  3. payParams.approval_url
  4. payParams.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_channelstripe / ezpay / paycools;生产代码也需要同步支持这些取值。如果要接其他通道,需要先由支付服务 B 的 /orders 支持该通道,再把后端 A 的渠道校验和前端选项一起放开。
  • 后端 A 会把 order_iduser_idorder_typeplan_idstatus=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_SECRETB 需要按双方约定签名

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=trueblockReason="daily_limit":每日免费消息用完。
  • POST /api/chat/send 返回 showUpgrade=true:照片付费墙或其他升级提示。
  • POST /api/chat/unlock-private 返回 showUpgrade=true:私密消息免费查看次数用完。
  • 用户主动点击会员中心/充值按钮。

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=paidpayment_successvip-status 的后端结果为准。 前端也不应该调用任何“确认支付成功”接口来改订单状态;订单状态只能由支付服务 B 接到支付通道回调后,通过 RabbitMQ 通知后端 A 更新。

8.3 支付中状态

创建订单后到支付结果返回前,按钮/弹窗应显示支付处理中,避免重复创建订单。用户关闭支付窗口时不要立即判定失败,继续轮询一小段时间或允许用户手动刷新状态。


9. 当前未生效或依赖外部配置的内容

这些内容已经有后端代码或接口定义,但上线生效需要对应外部条件:

功能 代码状态 生效条件 未满足时表现
付费墙展示 已接入聊天、图片、私密消息 无额外条件 非 VIP 返回 blockedshowUpgrade
创建订单 已有 /api/payment/create-order 配置 PAYMENT_SERVICE_URL,支付服务 B 可访问,且 B 返回 order_id + 可拉起支付的 pay_params 返回 502,或前端拿不到支付链接
支付结果发放权益 已有 RabbitMQ 消费者 配置 RABBITMQ_URLB 发布 payment.events 消息 订单停留 pendingVIP/DOL 不会到账
订单轮询 已有 /api/payment/order-status 执行 database/payment-migration.sql 创建 orders 查询不到订单或写入失败
VIP 解锁付费功能 已有 VIP 判定 users.is_vip=true 且未过期,或永久会员 非 VIP 仍按限制处理
DOL 充值到账 已有 DOL 发放 users.dol_balance 字段存在,B 回传成功 MQ 前端不能只靠创建订单展示到账
通话强制语音 /ws 已支持 call_text / call_audio TTS/STT 服务和前端通话 UI 接入 STT/TTS 失败时返回 call_error 或无语音

VIP 当前会解除/放宽的功能:

  • 每日免费消息次数限制。
  • 私密消息查看限制。
  • AI 男友照片查看和主动发图。
  • 语音/TTS 相关门控中需要 VIP 的普通语音回复。

10. 通话与语音补充

近期 /ws 增加了通话消息类型,供前端通话 UI 使用:

type 方向 说明
call_start 前端 -> 后端 进入通话态
call_end 前端 -> 后端 结束通话态,后端返回 call_ended
call_text 前端 -> 后端 前端已完成本地 STT,直接把文字发给后端
call_audio 前端 -> 后端 前端上传 base64 音频,后端 STT 后再进入 AI 回复
call_stt_result 后端 -> 前端 后端 STT 识别出的文字
call_stt_empty 后端 -> 前端 没识别到有效文字
call_error 后端 -> 前端 音频、STT 或通话处理错误

call_textcall_audio 会以 force_voice=true 进入聊天处理流程,目的是让通话场景生成可播放语音。普通文字聊天仍按原有语音门控和配额逻辑处理。


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.*photosend.*pictureshow.*picturelet me see youwhat 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.sqlorders 表,用于本地订单状态、轮询和 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_dateElio 主动发图日限)

上线前需在 Supabase SQL Editor 执行以上脚本,并创建 elio-schedules 公开 Storage Bucket。 若支付功能要闭环,必须确认 orders 表、users.is_vipusers.vip_expires_atusers.dol_balance 均已存在。