运行您的工作区
计费和计划
账单通过 Laravel Cashier 由 Stripe 支持。每个工作区一次只能使用一个套餐。 本页涵盖存在的套餐、如何计量使用情况,以及达到限制时会发生什么。
套餐
套餐由平台管理员管理(详见 套餐和 Stripe 同步),
客户在 /app/billing 可见。套餐有:
| 字段 | 作用 |
|---|---|
name | 显示名称(免费、专业版、企业版)。 |
slug | 稳定标识符——创建后永不更改,即使名称更改。 |
monthly_conversations | 每个日历月的新对话配额。0 表示无限制。 |
monthly_messages | 可选。每条消息配额,计算本月每个访客回合。留空表示没有额外上限;仅对话数控制工作区。 |
max_tokens_per_response | 可选。限制此套餐上每个回复的 LLM 的 max_tokens。留空使用默认值 800。适用于保持免费层简短和付费层详细。 |
price_cents | 套餐价格(美分)(每个 interval 收取一次)。0 表示免费/自定义(跳过网关同步)。 |
interval | 计费节奏——month 或 year。默认为 month。Stripe Prices、PayPal billing_cycles 和 Razorpay periods 都从此列派生。 |
features.remove_branding | 隐藏小部件中的"Powered by"页脚。 |
月度和年度变体
要提供年度折扣,创建相同的套餐两次——一个 interval=month
和一个 interval=year ——并将年度价格设置为低于月度的 12 倍。
营销定价页面检测两个变体并渲染月度/年度切换。每个网关同步到其本机节奏:
- Stripe ——Price 上的
recurring.interval = month|year。 - PayPal ——
billing_cycles[].frequency.interval_unit = MONTH|YEAR。 - Razorpay ——Plan 上的
period = monthly|yearly。
工作区仍然一次订阅一个套餐行(一个 workspaces.plan_id),
从月度切换到年度是正常的套餐更改——网关要么按比例分配(Stripe / PayPal),
要么根据工作区设置在下一个计费边界开始新周期。
AI 速率限制
两个可选的上限旋钮(monthly_messages 和
max_tokens_per_response)位于套餐表单中的"AI 速率限制"下。
它们在运行时强制执行:
-
每个访客消息在
usage_events中记录一个message行。MeteredBilling::canSendMessage()对当前日历月求和, 当总数达到monthly_messages时,用message_quota_exceeded错误事件短路 SSE 流。 -
MessageStreamController每轮读取一次maxTokensFor()(便宜——从工作区套餐的一行,控制器已经在加载的请求上), 并将其贯穿工具解析循环和最终流式调用。
Stripe 同步
当管理员创建或更新付费套餐时,StripeProductSync 服务确保存在匹配的
Stripe Product + Price。客户在结账前从不直接与 Stripe 打交道——他们在互客鱼 UI
中选择套餐并通过 Cashier 发送到 Stripe Checkout。
价格更改时,旧的 Stripe Price 被归档并创建一个新的(Stripe Prices 是不可变的)。 现有订阅保留在旧价格上;新订阅使用新的。这是每个 Stripe 原生 SaaS 使用的相同行为。
订阅
从 /billing,具有 billing.manage 权限的工作区成员可以:
- 从比较表中选择套餐。
- 被重定向到 Stripe Checkout。
- 支付;Stripe 重定向回
/billing并带有成功 flash。 - Stripe webhook 更新工作区的
plan_id+ 创建plan_subscription行。
存档卡片通过 Stripe 的客户门户管理。/billing 上的 管理卡片 按钮打开它。
配额
免费套餐限制每月新对话。执行在热路径上——每个 /v1/widget/init
调用询问 MeteredBilling::canStartConversation() 工作区是否低于其套餐限制。
如果不是:
{
"error": {
"code": "plan_limit_reached",
"message": "This workspace has reached its monthly conversation limit. Upgrade to continue."
}
}
返回 429。小部件的加载器在看到此内容时优雅地隐藏启动器——访客不会看到损坏状态。
什么算作对话
每个不同的对话行计为 1,由 IncrementUsageJob 在对话的第一轮完成时触发。
Playground 对话(is_playground=true)不计入,因此智能体的所有者可以自由测试。
恢复的对话不再计数——只有原始 init 会增加计量器。
品牌移除
features.remove_branding = true 的套餐隐藏小部件中的
"Powered by 互客鱼" 页脚。免费套餐附带品牌;付费套餐通常关闭。
Plan 模型将其公开为 $plan->removesBranding(),在 init 时调用。
发票
Stripe 将发票发送到存档的账单邮箱。完整历史记录可在 Stripe 客户门户中获得
(管理卡片 → 发票)。Cashier 还在服务器端公开 $workspace->invoices(),
如果您想在应用中渲染它们。
生命周期:取消、恢复、交换
面向客户的控件位于 /app/billing:
- 取消订阅。Stripe 在当前周期结束时安排取消(您在此之前保持访问)。 PayPal 立即取消(PayPal 使 CANCELLED 成为终端状态)。 Razorpay 在周期结束时安排取消。
-
恢复订阅。仅限 Stripe,且仅在取消尚未生效时(仍在 Cashier 的
onGracePeriod内)。PayPal CANCELLED 无法恢复;您再次订阅。 Razorpay 同样不支持在已取消的订阅上恢复。 -
套餐交换(升级/降级)。Stripe 进行就地交换,并在下一张发票上按比例分配。
PayPal 和 Razorpay 没有干净的就地交换,因此点击另一个套餐会取消当前订阅并重新进入结账。
CheckoutController的孤儿清理分支确保您永远不会同时支付两个订阅。
结账后对账
SubscriptionReconciler 服务是 webhook 交付的安全网。
成功结账后,客户重定向到 /app/billing?checkout=success
(Stripe 还附加 session_id={CHECKOUT_SESSION_ID}),
控制器直接从网关拉取实时订阅状态,带内翻转 workspace.plan_id。
即使在以下情况下,页面也会渲染正确的套餐:
- Stripe webhook 端点尚未在客户的 Stripe 仪表板中注册(在全新安装中非常常见)。
- webhook 触发但我们的端点暂时宕机/签名不匹配/被上游 WAF 拒绝。
- webhook 最终到达但需要 30+ 秒,在此期间客户重新加载账单页面并恐慌。
对账器是幂等的——每次页面加载都可以安全调用。webhook 在到达时仍然做同样的工作;
两条路径汇聚到 plan_subscriptions 中的同一行。
自定义套餐
price_cents = 0 的套餐在客户意义上不是免费的——它们是仅限本地的,
从不同步到 Stripe,用于手工打造的企业交易或替换免费套餐。管理员以相同方式创建它们;
Stripe 同步只是跳过。