111 lines
4.9 KiB
Markdown
111 lines
4.9 KiB
Markdown
# zcbot 控制台 iframe 嵌入对接
|
|
|
|
> 给 **platform 工程**:把 zcbot 控制台(`/static/dev.html`)嵌入 platform 网页。移动 App 套壳**不用 iframe**,另见 `APP.md`。
|
|
>
|
|
> 假设两边不同源;同源直接共享 localStorage,不需要这套握手。
|
|
|
|
## 一句话总结
|
|
|
|
```html
|
|
<iframe src="https://zcbot.example.com/static/dev.html?embed=1&parent_origin=https://portal.example.com"></iframe>
|
|
```
|
|
|
|
iframe 加载完发 `postMessage({type:"zcbot-ready"})`;platform 后端拿 `PLATFORM_KEY` 调 `POST /v1/auth/login` 换 JWT,`postMessage({type:"zcbot-token", token, user_id})` 推回 iframe。完事。
|
|
|
|
embed 模式下:brand / 退出登录 / 登录页隐藏,"+ 新建任务"挪到任务面板。
|
|
|
|
---
|
|
|
|
## 1. URL 参数
|
|
|
|
| 参数 | 必填 | 说明 |
|
|
|---|---|---|
|
|
| `embed` | 是 | 固定 `1` |
|
|
| `parent_origin` | 是 | platform 页面 origin,postMessage 白名单;缺失 / 不匹配 → iframe 显错误、消息全丢 |
|
|
| `task_id` | 否 | 首次签发 token 后自动定位到该 task(仅初次进入生效) |
|
|
|
|
## 2. postMessage 协议
|
|
|
|
两端都必须校验 `event.origin`。
|
|
|
|
iframe → platform:
|
|
|
|
| `type` | 时机 | platform 应做 |
|
|
|---|---|---|
|
|
| `zcbot-ready` | 加载完成 | 后端换 JWT → 回推 `zcbot-token` |
|
|
| `zcbot-401` | token 过期 / 被吊销 | 重新换 JWT 回推,或关闭 iframe |
|
|
|
|
platform → iframe:
|
|
|
|
| `type` | 字段 |
|
|
|---|---|
|
|
| `zcbot-token` | `token`(JWT)、`user_id`(UUID)、`user_name`(可选) |
|
|
|
|
## 3. platform 后端换 JWT
|
|
|
|
```
|
|
POST https://zcbot.example.com/v1/auth/login
|
|
{"user_id": "<UUID,platform 决定>", "platform_key": "<env PLATFORM_KEY>"}
|
|
→ {"token": "...", "expires_at": "...", "user_id": "...", "role": "user", "ttl_seconds": 604800}
|
|
```
|
|
|
|
- `PLATFORM_KEY` 是后端间共享密钥,**绝不能出现在浏览器**;换 token 必须 platform 后端代理。
|
|
- `user_id` 任意 UUID,首次出现自动建 users 行;platform 用户 ↔ zcbot user_id 映射由 platform 维护(建议 1:1)。可选带 `name` / `user_name`,每次登录 upsert(平台侧改名自动同步)。
|
|
- **滑动续签**:token 临期时,任一带 Bearer 的响应会带 `X-Refreshed-Token` / `X-Token-Expires-At` 头,见到就替换本地缓存(iframe 内已自动处理)。持续使用不掉线,`zcbot-401` 只在静默超过整个 TTL(默认 7 天)后发生。
|
|
|
|
```python
|
|
@app.post("/api/zcbot-token") # platform 自己的 API,由前端登录态保护
|
|
async def issue_zcbot_token(user_id: str = Depends(current_user_id)):
|
|
async with httpx.AsyncClient(timeout=5) as c:
|
|
r = await c.post("https://zcbot.example.com/v1/auth/login",
|
|
json={"user_id": user_id, "platform_key": os.environ["ZCBOT_PLATFORM_KEY"]})
|
|
if r.status_code != 200:
|
|
raise HTTPException(502, "zcbot login failed")
|
|
return {"token": r.json()["token"], "user_id": user_id}
|
|
```
|
|
|
|
## 4. platform 前端
|
|
|
|
```html
|
|
<iframe id="zcbot-frame" style="width:100%;height:100vh;border:0"
|
|
src="https://zcbot.example.com/static/dev.html?embed=1&parent_origin=https://portal.example.com"
|
|
allow="clipboard-read; clipboard-write"></iframe>
|
|
<script>
|
|
const ZCBOT_ORIGIN = "https://zcbot.example.com";
|
|
const frame = document.getElementById("zcbot-frame");
|
|
async function pushToken() {
|
|
const r = await fetch("/api/zcbot-token", { method: "POST", credentials: "include" });
|
|
if (!r.ok) return;
|
|
const { token, user_id, user_name } = await r.json();
|
|
frame.contentWindow.postMessage({ type: "zcbot-token", token, user_id, user_name }, ZCBOT_ORIGIN);
|
|
}
|
|
window.addEventListener("message", (e) => {
|
|
if (e.origin !== ZCBOT_ORIGIN) return;
|
|
const t = e.data && e.data.type;
|
|
if (t === "zcbot-ready" || t === "zcbot-401") pushToken();
|
|
});
|
|
</script>
|
|
```
|
|
|
|
iframe 高度必须足够(推荐 `100vh`,zcbot 内部有全屏 fixed modal,太矮会挤瘪)。
|
|
|
|
## 5. 安全要点
|
|
|
|
1. 生产必须 HTTPS;`parent_origin` 与实际 origin 严格一致。
|
|
2. 两端 message 处理都校验 `event.origin`,防伪造。
|
|
3. zcbot 部署侧:CORS `allow_origins` 收紧到 platform 域(`web/app.py`,目前 `["*"]`);建议加 CSP `frame-ancestors https://portal.example.com` 响应头。
|
|
4. env `PLATFORM_KEY` / `JWT_SECRET` 必填(启动 fail-fast 校验)。
|
|
|
|
## 6. 调试与故障
|
|
|
|
本地:zcbot 跑 `8765`,开 `http://127.0.0.1:8765/static/dev.html?embed=1&parent_origin=<iframe 自己的 origin>`,在 DevTools 里 `window.postMessage({type:"zcbot-token", token:"<curl 换的 JWT>", user_id:"<UUID>"}, location.origin)` 自发自收。
|
|
|
|
| 现象 | 处置 |
|
|
|---|---|
|
|
| 永远"等待登录…" | platform 没收 / 没回 `zcbot-ready`;查 `event.origin` 是否匹配 |
|
|
| 收到 token 仍不进主界面 | origin 不匹配或 `token`/`user_id` 字段缺失(静默丢弃),看 console |
|
|
| 一动作就跳回等待页 | JWT 过期 / `JWT_SECRET` 变过,重新换 token 回推 |
|
|
| modal 被挤瘪 | iframe 高度不够,改 `100vh` |
|
|
|
|
> 企业微信免登(`?embed=1&wecom=1`)是另一条 token 来路,与 platform iframe 对接无关,配置见 `RUN.md`。
|