Skip to content

模式 B 薄壳 SaaS 接入指南(浏览器前端直连)

读者:要新起一个自营 / 孵化 SaaS(自有前端、无自建用户体系)的工程师或 AI coding agent。 形态:自有前端 + OIDC 胶水;浏览器持平台 JWT 直连 /api/v1/app/conversations*(ADR-0052 双鉴权的 JWT 通道);用户管理、充值、租户后台零代码通道仲裁(ADR-0058):API + 模式 B 是自营 SaaS 唯一使能路线;代码级垂直通道已冻结。服务端集成视角(模式 A,API-Key + 影子用户)见 saas-integration-cookbook.md——那是外部二开方的通道,自营站点不要走。 交付物坐标(SM2-1):模板包 frontend/packages/web-saas-template/ · bootstrap 脚本 frontend/scripts/new-saas-app.mjs · 部署片段 frontend/packages/web-saas-template/deploy/compose-snippet.yml契约事实源openapi.yaml;本文冲突时以 openapi 为准。


§0 一分钟定位:你需要写什么、不写什么

你写你不写
前端页面(从模板派生:导航 / 文案 / 品牌定制)登录 / 注册 / 忘记密码(Keycloak 原生托管)
(仅当有自有领域实体时)最小自有后端 + HTTP Tool用户表 / 钱包 / 充值 / 租户后台(平台全部现成)
Keycloak client 与 env 配置影子用户同步 / API-Key(那是模式 A)

§1 前置:Keycloak SPA client

共享 realm(ADR-0003,realm 名 agenthub)下为你的 SaaS建一个 public SPA client:

  1. 建 client:Client ID 建议 agenthub-<你的saas名>;Type = public(SPA,PKCE);Root/Redirect URI = https://<你的域名>/callback;Web Origins = 同域。
  2. 四个 protocol mapper(与平台既有 client 同款;scripts/seed-testbed.sh 的 mapper 工厂是形状参照):
    mapper类型说明
    aud-<client_id>audience把 client id 加进 token aud(中间件受众校验需要)
    tenant_iduser attribute → claim活跃运营域(单值,SM1-2 切换端点改写)
    user_iduser attribute → claim平台侧该域内的本地用户 ID
    permissionsuser attribute → claim(多值)平台 scopes(JIT/claim sync 自动维护)
  3. scope 约定saas_user 角色自带 conversation:read/create/end 等;模板的页面门禁用 conversation:read。若你的前端有额外平台操作,经 Builder 的角色配置追加,不要在 SaaS 侧造平台 scope。

§2 bootstrap:从模板派生

bash
cd frontend
node scripts/new-saas-app.mjs --name edu-saas --target ../edu-saas

脚本复制模板并替换占位;随后二选一处理 workspace 依赖(@agent-hub/shared / @agent-hub/ui): ① 源码内联(完全独立仓)或 ② 私有 registry 版本依赖(多 SaaS 共享升级)。诚实边界:脚本不做这一步(见模板 README)。

env 表(.env 或构建参数):

变量必填说明
VITE_KEYCLOAK_CLIENT_ID§1 建的 client
VITE_KEYCLOAK_URL / VITE_KEYCLOAK_REALM对外可达的 Keycloak 地址 / agenthub
VITE_API_BASE_URL建议空同源经 nginx 反代 /api → 平台后端
VITE_PROJECT_ID运营给你建的项目 ID(归因维度;域内余额共享)

§3 身份接线:OIDC 三页 + JIT + 多归属切换

模板已内置(派生后无需改动,理解语义即可):

  • OIDC 三页LoginPage(PKCE 授权跳转)/ CallbackPage(换 token → 入 store → 静默续期 → JIT GET /api/v1/me)/ ProtectedRoute(未登录回登录页)。首登 JIT 会自动建平台侧 users 行 + 个人租户 + 回写 Keycloak 属性;token_refresh_required=true 时 Callback 会再刷一次 token 拿全 scopes。
  • 多归属切换(SM1-2 交付,SM2-1 消费):顶栏 TenantSwitcher(单归属自动隐藏)——
    • GET /api/v1/me/memberships:列出当前凭据的全部域归属(is_active 标记活跃域);
    • 切换 = POST /api/v1/me/active-tenant403 反枚举:未归属 / 域不存在 / 域停用返回同一响应,前端不得区分提示)→ 响应恒 token_refresh_required: true → 客户端必须强制 refresh token(旧 token 剩余 TTL 内仍按旧域服务,窗口期最后写入者赢)→ 清空本地会话历史与查询缓存 → 回首页。
    • 每域独立:users 行 / 用户空间 / 钱包(跨域 RLS 硬隔离,域内余额共享)。

§4 会话接线(平台 JWT 直连)

与 cookbook 同一组端点、另一条凭据通道(浏览器 JWT,仅本人;X-AgentHub-Api-Key 只属服务端):

  • 目录:GET /api/v1/app/agents?agent_type=conversation(五字段,响应键一律 snake_case:id/name/description/points_per_turn/idle_timeout_seconds——camelCase 是前端类型层的别名,不是 API 形状);
  • 开会话(幂等):POST /api/v1/app/conversationsclient_conversation_key
  • 轮次流:POST /api/v1/app/conversations/{id}/turns → SSE(turn_started/turn_delta/turn_card/turn_file/turn_completed;模板用 streamSse 因为 EventSource 不支持 POST);
  • 断连重拉:GET .../turns/{turn_id}(不重复扣点);历史分页 GET .../messages;结束 POST ...:end
  • 语音 / 附件 / 文生图按需启用(cookbook §7~§11 的端点同通道可用)。

§5 充值与扣点

  • 扣点:每轮按发布单价 freeze/settle(402 引导充值);域内所有 SaaS 共享同一钱包余额(SM1-1 域内合一)。
  • 充值(当前形态):平台充值链路已完成代码收口(SM3-1:微信 JSAPI/Native 全链 + GET /api/v1/billing/recharge-plans 服务端权威价目);投产开闸(真实商户凭据联调 + 真栈冒烟,ADR-0032 闸门)仍为运维外部前置。开闸前模板的积分页 = 余额展示 + 手动入账指引:运营执行 tenant_grant(或平台侧 admin_grant),入账即生效、流水按项目归因。注意 tenant_grant 请求体 expires_at 必填(长效积分到期时间,ISO-8601)——完整示例见 hello-agent 教程 §5.5.1
  • 对账:SaaS 侧若需自有订单 ↔ 平台积分对账,用 operation_id 幂等键(ADR-0044 双信任模型):你侧生成并记录 UUID,入账/消费流水可凭它对齐。

§6 部署

deploy/compose-snippet.yml 的 service 块加进 compose 编排(每个薄壳一个块,一个端口);nginx 已配 SSE 必需的 proxy_buffering off 与 3600s 读超时,勿删。TLS / 域名由 gateway 层统一。

§7 「从零到扣点」15 步 checklist

#步骤完成判据
1Keycloak 建 SPA client(public + PKCE)client 出现在 realm clients 列表
2加 4 个 mapper(aud / tenant_id / user_id / permissions)试发 token 含四 claim
3配 Root/Redirect URI + Web Origins/callback 可回跳
4运营建租户(= 你的运营域)拿到 tenant id
5运营在 Builder 建项目(挂该租户)拿到 project id(给 VITE_PROJECT_ID
6运营建会话 Agent(人设/模型/单价/护栏)Builder 里可见
7发布 Agent版本状态 published
8node scripts/new-saas-app.mjs --name <x> --target <dir> 派生产物目录可 pnpm install
9处理 workspace 依赖(内联或 registry)+ 填 envpnpm dev 起服务
10VITE_USE_MOCKS=true 离线冒烟模板 e2e 三场景绿
11切真 env(Keycloak / API 地址)登录页可达
12注册首个用户 → 登录顶栏出现用户名;GET /api/v1/me 200
13运营 tenant_grant 手动入账首批积分运营侧 ledger 可见 manual_adjust 入账行(终端积分页 403 是已知 residual SM5-1,勿作判据)
14选 Agent 开会话、发一轮收到流式回复;余额按单价减少
15compose 部署上线(§6)生产域名全链可走

全程没有一步需要写用户管理 / 充值 / 租户后台代码——需要时回看 §0 的边界表。