Appearance
品牌线绑定模型:品牌 · realm · 用户 · 租户 · 项目 · URL 的六条关系
读者:正在接入 AgentHub 的品牌产品线团队(人类工程师或 AI coding agent)——想搞清楚「品牌、身份、账户、产品」各由谁建立、记录在哪、靠什么生效。 定位:概念文档(Explanation)——心智模型与绑定关系速查,不含操作步骤;操作走 接入总纲 / 品牌自托管八步 / 模式 B 指南 / how-to。 状态基准:2026-09-15。角色权限表对齐代码事实源
backend/app/identity/service/platform_scope_catalog.py;邀请接口形状对齐openapi.yaml。 适用范围:模式 B(浏览器 JWT 直连)+ 品牌自托管边缘(默认部署形态)。
0. 一页纸心智
realm 管「哪条线」(品牌归属)· 注册管「谁」(身份)· 租户管「圈」(钱与数据的边界)· 邀请码管「进圈」(成员关系)· 项目管「账」(计费归因与会员独立)· 你的前端管「门面」(产品 URL)。
品牌域(DNS/服务器/边缘) ──► realm(身份名册) ──► 用户账号
│ JIT
▼
邀请码 ──► 租户(运营域/钱) ──► users 行 + 钱包(租户,用户)
│
▼
项目(归因/会员) ◄── VITE_PROJECT_ID ── 你的产品前端(品牌域路径)1. 品牌 ↔ realm:一条命令,一次成型
- 谁做:基座 owner / 运维,在基座节点执行
scripts/new-realm.sh <realm> <品牌域名> --client-id <realm>-web。 - 一条命令全部带出:Keycloak realm(强制
frontendUrl=https://<品牌域>/auth,签发者从此钉死在品牌域)+ 一个 PKCE 公共 SPA client + 5 个 protocol mapper(aud/realm-roles/tenant_id/user_id/permissions)+ 基座keycloak_realms信任注册表登记 + 注册开关(邮箱即用户名、强制邮箱验证、Google IdP 占位、独立 SMTP)。 - 时序:域名 / DNS / 边缘服务器不存在之前 realm 建不了;建成后 api 约 300 秒自动生效(栈启动时注册表为空的首个 realm 除外,需重启 api)。
- 一次性:此后这条品牌线的所有身份设施不再需要任何 Keycloak 管理操作。
- 品牌资料载体(可选增强,与上面的一条命令无关):品牌名 / slogan / 简介 / 明暗 logo / 明暗条幅不在 Keycloak 里,存基座
brand_profiles表——每 realm 一行(ADR-0067)。首行由基座 ops 建运营域租户时勾选「设为品牌运营租户」造册(或运维 seed),此后运营租户管理员在品牌域 builder「品牌设置」页或经PATCH /api/v1/brand/profile维护;渲染接线(博客品牌呈现 BA2 / KC 登录主题)完成前,录入内容不出现在任何对外页面(「可录不可见」)。
2. 用户 ↔ realm:入口即归属,无选择动作
- 谁做:终端用户自己——打开品牌域网站(如
https://<品牌域>),点注册 / 登录。 - 机制:品牌前端构建时烙死
VITE_KEYCLOAK_REALM/VITE_KEYCLOAK_CLIENT_ID;登录跳转 URL 的路径段(/auth/realms/<realm>/...)就是「选 realm」这个动作的全部——经品牌边缘/auth/反代到基座 Keycloak,注册页由该 realm 渲染,账号物理创建在该 realm 的名册里(容器关系,非事后绑定)。 - 推论:注册页上没有 realm 选择器;同一邮箱在两条品牌线 = 两个互不相认的独立账号(三 realm 三本名册,平台明确接受);不存在「把用户迁移到另一个 realm」的操作。
3. 用户 → 租户管理员:JIT 开户 + 升格
- JIT(自动,首登):登录回调后前端调一次
GET /api/v1/me,基座自动三件事——建平台侧users行(keycloak_user_id引用 Keycloak 用户 UUID)+ 建个人租户(自动写keycloak_realm_id=<realm>)+ 把tenant_id/user_id/permissions回写进 Keycloak 用户属性(经 mapper 进入后续每个 token)。R1 不变式(token realm ≠ 租户 realm → 403)两头对齐。 - 升格(人工一次):用户在 Builder 租户引导页调
POST /api/v1/tenants,把个人租户原地升级为正式运营租户,本人角色变为tenant_admin(本租户管理员)。升格后需重新登录一次刷新 token,新权限才生效。 - 边界:tenant_admin 只是本租户内的管理员——碰不了其他租户的数据(RLS 硬隔离),也碰不了平台运维面。
4. 用户 → 加入指定租户:邀请码兑换(当前唯一自助通道)
管理员侧(需
tenant:user_manage;Builder「设置 → 租户控制台」或直接调 API):bashPOST /api/v1/tenants/current/invites { "role_code": "saas_user", # 入域角色,默认普通成员,可给 builder "max_uses": 100, # 一码最多 100 人用(1~100,默认 1) "expires_in_hours": 168 } # 有效期(1~720 小时,默认 72)码经任何渠道发给申请者;C 端批量拉新用大
max_uses的共享码。申请者侧:
POST /api/v1/tenants/invites/redeem(带码)→ membership 建立,角色按码内role_code→POST /api/v1/me/active-tenant切换活跃租户(响应恒token_refresh_required: true,前端强制刷新 token)→ 此后请求以该租户身份发出,域内钱包(该租户, 用户)开户。一个用户可同时是多个租户的成员(多归属),同一时刻只有一个活跃租户。注意:新用户在受邀前只属于自己的个人租户,看不到运营租户的 Agent 目录(RLS);入域 ≠ 有积分——能开聊还需运营租户管理员(持
billing:points:tenant_grant,经 builder 控制台的「发放积分」入口)给用户本人发过积分(真收款开闸前的入账方式)。
5. 管理员 / builder → 项目:品牌域下的 Builder 创建,两种角色都能建
- 入口:品牌域下的 Builder 控制台(
https://<品牌域>/builder/——按品牌参数重新构建的同一份 SPA,与门户共用线级 client、同源免二次登录)。不是 基座域的/builder/:那是平台 realm 的门,品牌账号进不去(例外:S0 演练期在平台 realm 走七步流程,用平台 realm 账号)。 - 权限:
POST /api/v1/builder/projects要求 scopebuilder:project:write,持有者为tenant_admin和builder两个角色:
| 角色 | 来源 | 建项目/Agent/模型/KB | 租户管理·邀请·发积分·M2M·内容专栏 | 会话消费 |
|---|---|---|---|---|
tenant_admin | 租户升格时的创始人 | ✅ | ✅ | ✅ |
builder | 邀请码 role_code=builder | ✅ | ❌ | ✅ |
saas_user | 邀请码默认角色 | ❌ | ❌ | ✅ |
- 产出:项目创建成功返回
project_id(UUID)——仅此而已,平台不生成任何 URL。一个租户可建多个项目(= 多产品):同租户内用户与充值共享、会员与报表按项目独立。
6. 消费者 → 使用产品:URL 是你自己造的,nginx 与项目 id 无关
- 产品 URL 的三个组成部分全部出自品牌方:域名(品牌域)+ 路径(派生前端时的
VITE_BASE_PATH,如/english/)+ 内容(从web-saas-template派生、构建、部署在自己服务器上的前端)。 - nginx 路由的职责边界:只做「路径 → 本机前端服务」的转发(
location /english/),不知道也不需要知道 project_id。 - project_id 的真实位置:构建期烙进前端包的常量(
VITE_PROJECT_ID);前端调 API 时携带,平台按它做计费归因。用户浏览器地址栏里永远不会出现它。 - 请求全链路:用户开
https://<品牌域>/english/→ 边缘直出你的前端 → 前端经/api/(边缘反代到基座)调GET /app/agents拉该租户已发布 Agent → 开会话(SSE 流式)→ 每轮按单价扣(租户, 用户)钱包、流水记到项目。 - 消费前置:打开 URL 只算进了门;真正能聊需要 ①是该运营租户成员(§4)②钱包有积分,否则 402 引导充值。
7. 六条串起来的最小操作剧本
基座运维跑一次 new-realm.sh(§1)→ 你在品牌域注册(§2)→ 升格运营租户成为 tenant_admin(§3)→ 发邀请码拉进用户与 builder(§4)→ 在品牌域 Builder 建项目拿 project_id、创建并发布 Agent(§5)→ 派生部署前端、把 project_id 焊进构建(§6)→ 发积分、开聊。全链 worked example 见 first-saas 教程。
8. 常见误区速查
| 误区 | 事实 |
|---|---|
| 注册时让用户「选择」realm / 品牌 | 没有这个步骤——访问哪个品牌域,就自动在哪个 realm 注册(§2) |
品牌运营者登录基座域的 /builder 管项目 | 进不去——那是平台 realm 的门;品牌线用品牌域下的 Builder(§5) |
| 平台会生成/展示项目的访问 URL | 平台只发 project_id;产品 URL = 你自己部署的前端地址(§6) |
| nginx 按项目 id 路由 | nginx 只按路径转发到前端服务;project_id 是前端构建期常量,不参与路由(§6) |
| 邀请入域后用户就能消费 | 还需给用户本人发积分;402 = 引导充值(§4/§6) |
| 同一邮箱可跨品牌线通用 | 不支持——三 realm 三本名册;同线多产品才共享账号(§2/§5) |
| 把用户迁到另一个 realm | 无此操作;换线 = 在新 realm 重新注册(§2) |