# 从零发布你的第一个 SaaS（worked example：口语搭子）

> **状态基准**：2026-09-03（试点叙事仍是 2026-08-30 ECS-B 真栈；其后 PE 已补齐知识库 Tier B / 支付双通道代码 / 多 realm。真收款与 Google 登录仍未开闸，见 [platform-status.md](../platform-status.md)）。
> **读者**：要孵化第一个自营 SaaS 的建设者（人类工程师或 AI coding agent）。
> **性质**：本文是**真实跑通的完整叙事**——下面每一步都在 2026-08-30 的 ECS-B 真栈上实际发生过，数字、事件、回复摘录全部来自验收证据，不是示例假数据。机制的完整参数表在 [`saas-mode-b-integration-guide.md`](../saas-mode-b-integration-guide.md)（§7 有同源 15 步 checklist），本文负责把它讲成一个故事。
> **契约事实源**：`openapi.yaml`。

---

## 0. 你要造什么

「口语搭子」——一个职场英语口语 SaaS，两个会话 Agent：

| Agent | 人设 | 单价 |
|---|---|---|
| 日常口语陪练 | 咖啡馆场景日常对话陪练 | **10 分/轮** |
| 面试模拟官 | 产品经理行为面试英文模拟 | **15 分/轮** |

用户注册登录、钱包扣点、租户后台——**一行代码都不用写**。你写的只有：从模板派生的前端（品牌与页面定制）。

## 1. 平台侧准备（运营动作，15 步 checklist 的 1~7）

1. **Keycloak SPA client**（public + PKCE，试点 client id `agenthub-oral-buddy`，当时落在平台 realm `agenthub`）+ 四个 protocol mapper（`aud-…` / `tenant_id` / `user_id` / `permissions`）——形状参照 mode-b 指南 §1。**新品牌线不要照抄 realm=`agenthub`**：生产默认是每线一个 realm，见 [品牌自托管](../brand-self-hosted-deployment-guide.md)。验收实测：试发 token 的 `aud` 同时含 oral-buddy 与 console，四 claim 齐。
2. **建正式租户**：运营首登 JIT 自动建个人租户后，`POST /tenants` 升级正式租户（slug `oral-buddy`，kind=formal，name=口语搭子）。
3. **建项目**「口语搭子 Web」挂该租户——这是计费归因维度，ID 填进前端 `VITE_PROJECT_ID`。建项目的 curl 见 [hello-agent 教程 §2.1](./hello-agent.md)（`POST /api/v1/builder/projects`；或经 web-builder 界面 Projects 页创建。该端点契约收录登记为跟进项）。
4. **建模型组**（LLM-chat，空组走平台 fallback 即可起步）。
5. **建双 Agent 并发布**（人设 / 模型组 / 单价 10 与 15 / 护栏齐全——单价与护栏强制配对，缺一发布被拒）。

判据：`GET /api/v1/app/agents?agent_type=conversation` 返回的 **`items` 数组长度 = 2**（响应无 count 字段）、`points_per_turn` 10/15（响应键为 snake_case）。

## 2. 派生你的前端（checklist 8~11）

```bash
cd frontend
node scripts/new-saas-app.mjs --name oral-buddy --target ../oral-buddy
```

- workspace 依赖二选一（源码内联或私有 registry），填 env（`VITE_KEYCLOAK_CLIENT_ID` / `VITE_KEYCLOAK_URL` / `VITE_PROJECT_ID`…），`VITE_USE_MOCKS=true` 先离线冒烟。
- 部署：把 `deploy/compose-snippet.yml` 的 service 块加进编排（服务名 `web-oral-buddy`）。验收实测：`GET https://<网关>/oral-buddy/` → HTTP 200，登录页标题即你的品牌「口语搭子 · 职场英语口语陪练」。

## 3. 用户进来（checklist 12~13）

1. 终端用户在你的站点注册 → 平台 JIT 自动建域内用户行；被邀请入域（redeem + `active-tenant` 切换）后，活跃域 = 口语搭子。
2. 运营给首批种子积分：`tenant_grant` 手动入账（**现在仍然是对用户的标准形态**——PayPal / 微信真收款未开闸，见 [payment-and-recharge.md](../payment-and-recharge.md)）。验收实测：**+200 分**，`source=tenant_grant`，流水 `operation_id` 可对账（请求体 `expires_at` 必填）。

## 4. 见到钱动起来：双轮对话（checklist 14）

用户从目录选 Agent 开会话、各发一轮。验收实测（SSE 事件序列 `turn_started → turn_delta → turn_completed`）：

- **日常口语陪练（扣 10 分）**，真实上游英文回复开场：
  > "Hey there! Welcome to the café. What can I get for you today? We've got a great latte special on right now."
- **面试模拟官（扣 15 分）**：
  > "Welcome! For a product manager behavioral round, let's kick off with a 1-minute English self-introduction. …"

每轮 `turn_completed` 携带 `points_charged`（10 / 15）——前端展示即扣点回显。

## 5. 对账（checklist 15）：每分钱都能回答「花在哪、买了什么」

以运营身份读 `GET /api/v1/billing/points/ledger?user_id=<终端>&project_id=<项目>`，验收实测五行流水：

| 时间线 | type | amount | operation_id | 含义 |
|---|---|---|---|---|
| t0 | `manual_adjust` | +200 | `sm41-grant-…` | 种子入账 |
| t1 | `freeze` | 10 | `turn:25d4ddf8-…` | 陪练轮冻结 |
| t2 | `deduct` | 10 | `turn:25d4ddf8-…` | 陪练轮划扣 |
| t3 | `freeze` | 15 | `turn:1b1cc78a-…` | 面试轮冻结 |
| t4 | `deduct` | 15 | `turn:1b1cc78a-…` | 面试轮划扣 |

同一 turn 的 freeze/deduct 共享 `operation_id`；`cost_records` 另有两行 `call_kind=llm` 且 `project_id` 指向「口语搭子 Web」——**模型成本也按项目归因**。这就是「强治理执行」在你 SaaS 上的样子。

## 6. 常见卡点（真栈踩过的坑）

| 症状 | 原因与处置 |
|---|---|
| 终端用户积分页 403 | **旧环境**：种子角色未含 billing 只读 scope。2026-08-30 之后的 `saas_user` 已含 `billing:points:read` / `billing:ledger:read`，新环境应 200。仍 403 则重跑角色种子或查 token `permissions` |
| 发布被拒（409） | 单价与五项护栏强制配对，缺一不可（[cookbook](../saas-integration-cookbook.md) 计费章） |
| SSE 一次性整段到达 | 检查网关 `proxy_buffering off`（模板 nginx 已配，勿删） |
| 切换租户后行为怪 | `active-tenant` 响应恒 `token_refresh_required: true`，客户端必须强制 refresh token（mode-b 指南 §3） |

## 7. 你学到了什么

- 你的 SaaS = 业务壳；平台 = headless 会话运行时 + 身份 + 钱包（模式 B，ADR-0058 唯一路线）。
- 每一轮消费都可对账：`operation_id` 串起 freeze/deduct，`cost_records` 记录模型成本与项目归因。
- 下一步深入：[`conversation-runtime-protocol.md`](../conversation-runtime-protocol.md)（SSE 帧与状态机）→ [`knowledge-base-retrieval.md`](../knowledge-base-retrieval.md)（文档进 Agent）→ [`payment-and-recharge.md`](../payment-and-recharge.md)（目录与开闸边界）→ [`brand-self-hosted-deployment-guide.md`](../brand-self-hosted-deployment-guide.md)（上线一条产品线）→ [`saas-integration-cookbook.md`](../saas-integration-cookbook.md)（若后续需要自有后端）。
- 能对用户承诺什么：[`platform-status.md`](../platform-status.md)。
