互客鱼 返回主站

运行您的工作区

计费和计划

账单通过 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计费节奏——monthyear。默认为 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_messagesmax_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 权限的工作区成员可以:

  1. 从比较表中选择套餐。
  2. 被重定向到 Stripe Checkout。
  3. 支付;Stripe 重定向回 /billing 并带有成功 flash。
  4. 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。小部件的加载器在看到此内容时优雅地隐藏启动器——访客不会看到损坏状态。

现有对话和人工接管受限制。仅新的 init 调用。 因此,当您达到限制时正在进行对话的访客可以完成他们的线程。

什么算作对话

每个不同的对话行计为 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 同步只是跳过。