9.4 KiB
9.4 KiB
zcbot 控制台 iframe 嵌入对接
给 platform 工程:把 zcbot 控制台(
/static/dev.html)嵌入 platform 主页,需要做的事。假设:platform 和 zcbot 不同源(eg.
https://portal.example.com嵌https://zcbot.example.com)。同源直接共享 localStorage 不需要这套握手。
一句话总结
<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 后端拿 PLATFORM_KEY 调 POST /v1/auth/login 换出 JWT,通过 postMessage({type:"zcbot-token", token, user_id}) 推回 iframe。完事。
embed 模式下:左上 brand 隐藏 · 退出登录隐藏 · 登录页不显示 · "+ 新建任务"挪到任务面板。
1. URL 参数
| 参数 | 必填 | 说明 |
|---|---|---|
embed |
是 | 固定 1,触发 embed 模式 |
parent_origin |
是 | platform 主页 origin(scheme + host + 端口),postMessage 白名单。没传 / 不匹配 → iframe 显错误占位、所有 message 都被丢 |
task_id |
否 | task UUID。首次签发 token 后自动选中该 task 并加载其消息。仅在初次进入生效;后续用户在 UI 内切 task 或 401 重签都不会再回到此 task |
示例:
https://zcbot.example.com/static/dev.html?embed=1&parent_origin=https://portal.example.com
https://zcbot.example.com/static/dev.html?embed=1&parent_origin=https://portal.example.com&task_id=01HXXXXX-...
2. postMessage 协议
所有消息 data 都是 plain object,type 字段标识种类。两端都必须校验 event.origin。
iframe → platform(子发父)
type |
时机 | 字段 | platform 应做 |
|---|---|---|---|
zcbot-ready |
iframe 加载完成 / 已有缓存 token 但仍发一次 | (无) | 检查当前用户 → 调后端换 JWT → 回推 zcbot-token |
zcbot-401 |
用户操作触发 401(token 过期 / 被吊销) | (无) | 重新换 JWT(可能要求用户重新登录 platform)→ 回推 zcbot-token;或决定关闭 iframe |
platform → iframe(父发子)
type |
字段 | iframe 行为 |
|---|---|---|
zcbot-token |
token (string, JWT) user_id (string, UUID) user_name (string, 可选) |
写 localStorage + 进入主界面;若已在主界面(401 重签场景)只重载任务列表 |
3. platform 后端:换 JWT
zcbot 已经有 SSO 入口,platform 后端用共享的 PLATFORM_KEY 代任意用户换 JWT:
POST https://zcbot.example.com/v1/auth/login
Content-Type: application/json
{
"user_id": "<UUID,platform 决定>",
"platform_key": "<跟 zcbot env PLATFORM_KEY 同串>"
}
返回:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user_id": "...",
"exp": 1740000000
}
PLATFORM_KEY:platform 和 zcbot 后端共享的密钥,绝不能下放到浏览器。换 token 必须 platform 后端代理。user_id:任意 UUID;首次出现 zcbot 会自动建users行(占位,无 email/密码),后续 task / message 都用这个 user_id 作为分区键。platform 用户 ↔ zcbot user_id 映射由 platform 维护(建议 1:1)。- JWT 默认 7 天 TTL(可改
ZCBOT_JWT_TTL_SECONDSenv)。token 过期前 iframe 收到 401 → 发zcbot-401给父端 → 父端再走这个换 token 流程。
Node.js 后端示例
// POST /api/zcbot-token (platform 自己的 API,由前端登录态保护)
app.post("/api/zcbot-token", async (req, res) => {
const userId = req.session.zcbotUserId; // platform user → zcbot user_id 映射
const r = await fetch("https://zcbot.example.com/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: userId,
platform_key: process.env.ZCBOT_PLATFORM_KEY,
}),
});
if (!r.ok) return res.status(502).json({ error: "zcbot login failed" });
const { token } = await r.json();
res.json({ token, user_id: userId, user_name: req.session.userName });
});
Python (FastAPI) 后端示例
import os, httpx
from fastapi import HTTPException
@app.post("/api/zcbot-token")
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")
data = r.json()
return {"token": data["token"], "user_id": user_id}
4. platform 前端:iframe + postMessage
<iframe
id="zcbot-frame"
src="https://zcbot.example.com/static/dev.html?embed=1&parent_origin=https://portal.example.com"
style="width:100%; height:100vh; border:0;"
allow="clipboard-read; clipboard-write"
></iframe>
<script>
const ZCBOT_ORIGIN = "https://zcbot.example.com";
const frame = document.getElementById("zcbot-frame");
async function pushToken() {
// 调 platform 后端拿 zcbot JWT(后端用 PLATFORM_KEY 换)
const r = await fetch("/api/zcbot-token", { method: "POST", credentials: "include" });
if (!r.ok) {
console.error("zcbot token fetch failed");
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 内部有多个全屏 position:fixed modal,iframe 太小会挤瘪)。
5. 安全要点
parent_originURL 参数必填 —— 缺失 / 非法 iframe 不工作。这是 postMessage 的 origin 白名单。PLATFORM_KEY只在 platform 后端 —— 等同 JWT 签名权限,泄漏 = 任意伪造任意用户。前端代码 / Git 仓库 / 客户端日志都不能出现。- iframe 收 message 强制校验
event.origin === parent_origin—— 已在 dev.html 实现,父端也应对称校验event.origin === ZCBOT_ORIGIN,防恶意页面伪造消息。 - CSP
frame-ancestors—— 真发布时 zcbot 这边建议加响应头限制只允许 platform 域嵌入,目前 zcbot 未默认设置(本地宽松,见 §6)。 - HTTPS 必须 ——
parent_origin是https://,生产部署不要走 http(postMessage 在 http 不发警告,但中间人可改 iframe src 注入恶意 token)。 - TTL:JWT 默认 7 天。若 platform 希望更短(强制频繁刷新),改 zcbot env
ZCBOT_JWT_TTL_SECONDS即可。
6. zcbot 部署侧需要做的事
这一节是 zcbot 运维 / 我自己 要做的,platform 工程不需要操心,列出来供参考。
- CORS
allow_origins收紧到 platform 域名(目前["*"],见web/app.py:516)allow_origins=["https://portal.example.com"], allow_credentials=False, - CSP
frame-ancestors加响应头中间件,只放行 platform 域:
(FastAPI 默认不发@app.middleware("http") async def csp_headers(req, call_next): resp = await call_next(req) resp.headers["Content-Security-Policy"] = ( "frame-ancestors https://portal.example.com" ) return respX-Frame-Options,iframe 加载本身不会被浏览器拦;frame-ancestors是 CSP 替代品,更细粒度。) - env:
PLATFORM_KEY/JWT_SECRET必填(已是启动 fail-fast 校验,见web/auth.py:67)。
7. 调试
本地试 iframe(同机两端口):
- zcbot 跑
8765→ 浏览器开http://127.0.0.1:8765/static/dev.html?embed=1&parent_origin=http://127.0.0.1:3000 - 单独不带 platform 时 iframe 会显示"等待登录…",DevTools console 看
postMessage收发。 - 手动模拟 platform 推 token(在 iframe DevTools 里):
注意 origin 必须 matchwindow.postMessage({type:"zcbot-token", token:"<手工 curl 换的 JWT>", user_id:"<UUID>"}, location.origin)parent_origin参数,本机调试时把parent_origin设成 iframe 自己的 origin 即可(自发自收)。
curl 试后端换 token:
curl -X POST http://127.0.0.1:8765/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"user_id":"00000000-0000-0000-0000-000000000001","platform_key":"<env PLATFORM_KEY>"}'
8. 故障兜底
| 现象 | 原因 | 处置 |
|---|---|---|
| iframe 显示"embed 模式缺少 parent_origin 参数" | URL 里没传 parent_origin |
加上 &parent_origin=https://... |
| iframe 永远显"等待登录…" | platform 没收 / 没回 zcbot-ready |
DevTools console 看 message;校验 event.origin 是否匹配 |
iframe 收到 zcbot-token 但仍不进主界面 |
event.origin !== parent_origin 或 token / user_id 字段缺失 |
iframe 是静默丢弃,看 console 是否有报错;校验字段拼写 |
| 一动作就跳回等待页 | JWT 过期 / 后端校验失败,触发 zcbot-401 |
platform 重新换 token 回推;或检查 zcbot 端 JWT_SECRET 是否变过 |
| 嵌入页面 modal(新建任务 / 文件预览)被挤瘪 | iframe 高度不够 | iframe height 改 100vh 或更高,zcbot modal 是 position:fixed; inset:0 |
带 task_id 进来后没自动定位 / 报"加载失败" |
task_id 不属于当前签发 token 的 user_id,或 UUID 拼写错 | 校验 platform 传的 task_id 归属对的 user;chat 区错误信息会显具体 message |