zcbot/EMBED.md

4.9 KiB

zcbot 控制台 iframe 嵌入对接

platform 工程:把 zcbot 控制台(/static/dev.html)嵌入 platform 网页。移动 App 套壳不用 iframe,另见 APP.md

假设两边不同源;同源直接共享 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_KEYPOST /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 天)后发生。
@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 前端

<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