Files
deerflow-code/frontend-web/docs/embed-iframe-chat.md
T
2026-06-02 20:36:22 +08:00

11 KiB
Raw Blame History

工作台问答 · iframe 嵌入开发说明

本文档说明如何将 DeerFlow 通用工作台问答(与 /page/workspace/chats/* 同一套 LangGraph 流式能力)以普通 iframe 形式嵌入第三方页面,并在父页面无法读取子 frame 内 thread_id 的前提下,轮询「本次问答是否已结束」。

后端公开 API 实现见:offline-backend-20260512/backend/app/gateway/routers/public_embed.py
数据表:embed_sessions(迁移 20260602_01)


0. 速读

角色 要做的事
父页面 自己生成 sessionId(UUID),拼进 iframe src;用 sessionId 调公开 API 轮询 finished
iframe(DeerFlow) 打开 /embed/chats/...,自动登录、自动发问、登记 sessionId → thread_id
不需要 无界(wujie)、父页面拿 thread_id、DeerFlow 登录态 Cookie

完成条件:GET .../sessions/{sessionId}/status 返回 finished === true(且无进行中的 run,非澄清中断)。


1. 背景与约束

1.1 业务目标

  • 第三方系统用 iframe 嵌 DeerFlow 问答 UI。
  • 传入:问题文案、用户名(走现有 login/username)、主题色。
  • 展示:消息列表 + 沙箱/产物侧栏(与主聊天相同的 ChatBox);隐藏页面左侧栏、Workspace 侧栏、底部输入框。
  • 进入后 自动发送 首条用户消息并走流式问答。

1.2 为何需要 sessionId

iframe 与父页面通常 跨域,父页面 不能 读取子应用 URL 里的 thread_id(hash 路由在子 frame 内)。
因此约定:

  1. 父页面在打开 iframe 前生成全局唯一的 sessionId;
  2. 子页面在创建/确定 thread_id 后,调用后端 登记接口 写入 session_id → thread_id;
  3. 父页面只凭 sessionId 查询状态,必要时响应里会带上 thread_id(可选使用)。

2. 端到端流程

sequenceDiagram
  participant Parent as 父页面
  participant Iframe as DeerFlow iframe
  participant API as Gateway /api/public/embed

  Parent->>Parent: sessionId = crypto.randomUUID()
  Parent->>Iframe: iframe src 含 sessionId、message、username、theme
  Iframe->>Iframe: loginByUsername + 自动 sendMessage
  Iframe->>API: POST /sessions { session_id, thread_id }
  loop 每 1–3s
    Parent->>API: GET /sessions/{sessionId}/status
    API-->>Parent: registered, finished, thread_id
  end

3. 前端路由与页面

3.1 路由(独立于 /page/*)

在 App.tsx 注册,不经过 PageRoutes / PageSidebar,因此无外层导航与 Workspace 侧栏。

路由 组件 说明
/embed/chats/:thread_id EmbedChatPage(外包 ChatRuntime) thread_id 可为 new 或已有 UUID

示例(HashRouter,注意 #):

http://localhost:5173/#/embed/chats/new?sessionId=...&message=...&username=guest&theme=red

开发环境端口以本地 Vite 为准(如 5173 / 5174)。

3.2 URL 查询参数

参数 必填 说明
sessionId 或 session_id 是 父页面生成的关联 id,8–128 位,仅字母数字 _ -
message / question / text 建议 首条用户问题;有则进入后自动发送
username / userName 否 默认 guest,对应 POST /api/v1/auth/login/username
theme 否 dark-blue → 深色;red 等 → 浅色(与 LoginPage 一致)

缺少 sessionId 时,嵌入页会提示错误,不会发起登记。

3.3 相关源码

路径 职责
src/pages/EmbedChatPage.tsx 嵌入页:登录、自动发问、登记 session、仅消息列表 + ChatBox
src/core/embed/params.ts 从 URL 解析嵌入参数
src/core/embed/status-api.ts 登记 / 轮询 API 封装
src/App.tsx 路由注册

3.4 UI 行为说明

  • 隐藏:PageSidebar、WorkspaceSidebar、InputBox、顶栏标题栏等(相对完整 ChatPage)。
  • 保留:MessageList、流式指示、澄清卡片(若 agent 触发 ask_clarification)、右侧 产物/沙箱(ChatBox 内 60/40 分栏逻辑与主聊天一致)。
  • 线程 id:新建会话时客户端先分配 UUID,onStart 后 history.replaceState 更新 hash 为 /embed/chats/{真实id}(与主聊天 ChatPage 相同手法,避免整页重载)。

4. 公开 HTTP API(无需鉴权)

前缀:/api/public/embed/(走 Gateway,与 VITE_BACKEND_BASE_URL 一致,默认 http://127.0.0.1:8001)。

CSRF / Auth 中间件对 /api/public/ 放行;请求 不需要 Cookie。

4.1 登记映射(iframe 内自动调用)

POST /api/public/embed/sessions
Content-Type: application/json

{
  "session_id": "parent-generated-uuid",
  "thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b"
}

响应:

{
  "session_id": "parent-generated-uuid",
  "thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
  "registered": true
}

同一 session_id 再次 POST 会 更新 thread_id(例如 onStart 后 id 变化)。

4.2 轮询状态(父页面使用)

GET /api/public/embed/sessions/{session_id}/status

尚未登记(iframe 仍在加载或未 POST):

{
  "session_id": "parent-generated-uuid",
  "registered": false,
  "thread_id": null,
  "finished": false,
  "thread_status": "pending",
  "has_active_run": false,
  "latest_run_status": null
}

已登记且问答进行中:

{
  "session_id": "parent-generated-uuid",
  "registered": true,
  "thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
  "finished": false,
  "thread_status": "idle",
  "has_active_run": true,
  "latest_run_status": "running"
}

已登记且已结束:

{
  "session_id": "parent-generated-uuid",
  "registered": true,
  "thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
  "finished": true,
  "thread_status": "idle",
  "has_active_run": false,
  "latest_run_status": "success"
}

字段说明

字段 含义
registered 是否已收到 iframe 的 POST 登记
finished 父页面应以此为准:无 inflight run,且线程非 running/busy,且非 interrupted(澄清等待用户)
thread_status 从 checkpoint 推导:idle / interrupted / error 等
has_active_run 是否存在 pending/running 的 LangGraph run
latest_run_status 该线程最近一次 run 的状态:success / error / …
thread_id 解析出的 DeerFlow 线程 id(父页面可选用,非必须)

4.3 按 thread_id 查询(可选)

调试或父页面已持有 thread_id 时:

GET /api/public/embed/threads/{thread_id}/status

响应不含 session_id / registered,其余与上表类似。

4.4 前端封装

import {
  getEmbedSessionStatus,
  registerEmbedSession,
} from "@/core/embed/status-api";

// 父页面(或你们自己的脚本)
const status = await getEmbedSessionStatus(sessionId);
if (status.registered && status.finished) {
  // 问答结束
}

// 仅 iframe 内需要
await registerEmbedSession(sessionId, threadId);

fetch 使用 credentials: "omit",不携带 DeerFlow 会话。


5. 父页面集成示例

5.1 拼接 iframe

<iframe
  id="deerflow-embed"
  title="DeerFlow 问答"
  style="width:100%;height:600px;border:0"
></iframe>
const sessionId = crypto.randomUUID();
const base = "http://localhost:5173"; // 换成你们部署的前端 origin
const params = new URLSearchParams({
  sessionId,
  message: "请根据附件总结要点",
  username: "guest",
  theme: "red",
});
document.getElementById("deerflow-embed").src =
  `${base}/#/embed/chats/new?${params.toString()}`;

5.2 轮询直到结束

const API = "http://127.0.0.1:8001"; // VITE_BACKEND_BASE_URL

async function pollUntilFinished(sessionId) {
  const res = await fetch(
    `${API}/api/public/embed/sessions/${encodeURIComponent(sessionId)}/status`,
    { credentials: "omit" },
  );
  if (!res.ok) throw new Error(`status HTTP ${res.status}`);
  const data = await res.json();
  if (!data.registered) return { done: false, reason: "pending" };
  if (data.finished) return { done: true, data };
  return { done: false, reason: "running" };
}

const timer = setInterval(async () => {
  try {
    const result = await pollUntilFinished(sessionId);
    if (result.done) {
      clearInterval(timer);
      console.log("问答已结束", result.data);
    }
  } catch (e) {
    console.error(e);
  }
}, 2000);

5.3 跨域注意

  • 父页面 fetch 的是 Gateway 源(如 8001),不是 iframe 的 Vite 源;需 Gateway 配置 CORS 允许父页面 origin(若跨域)。
  • iframe src 的前端域名需与你们部署一致;后端地址由 DeerFlow 前端 .env 的 VITE_BACKEND_BASE_URL 决定。

6. 部署与数据库

  1. 跑迁移(SQL 持久化后端):

    cd offline-backend-20260512/backend
    # 按项目惯例执行 alembic upgrade head
    

    表:embed_sessions(session_id PK, thread_id, created_at, updated_at)。

  2. 重启 Gateway,确认路由已挂载:public_embed.router(app/gateway/app.py)。

  3. 前端构建 后,将嵌入地址配给第三方;VITE_BACKEND_BASE_URL 指向可达的 Gateway。

无 SQL 时开发环境会退化为 内存 MemoryEmbedSessionStore(进程重启后映射丢失);生产请使用持久化库。


7. 安全说明

  • sessionId 与 thread_id 均视为 不透明能力令牌:知晓即可查询状态(公开 API 无登录)。
  • 建议:父系统自行生成不可猜测的 UUID;敏感场景在贵司 API 网关再加鉴权或 IP 限制。
  • 登记接口为 POST upsert,不校验「session 是否属于某租户」——若需多租户隔离,请在业务层约定 sessionId 命名空间或增加网关校验。

8. 与主聊天 /page/workspace/chats 的差异

项目 主聊天 iframe 嵌入
路由 /page/workspace/chats/:id /embed/chats/:id
布局 完整 Workspace + 输入框 仅消息区 + 产物栏
登录 常规登录流 URL username 自动 login/username
首条消息 用户输入 URL message 自动发送
完成态查询 无公开 API sessionId + /api/public/embed/sessions/.../status

能力上仍走同一 useThreadStream + lead_agent 流式链路,沙箱与产物行为与主聊天一致。


9. 常见问题

Q:registered 一直为 false?
A:检查 iframe URL 是否带 sessionId;子页是否登录成功;Network 里是否有 POST /api/public/embed/sessions 且 200。

Q:finished 一直 false?
A:可能仍在流式生成(has_active_run: true),或 agent 进入澄清(thread_status: interrupted)。嵌入模式无底部输入框,澄清需产品侧另行处理或避免触发澄清类工具。

Q:父页面能否不用轮询?
A:当前标准方案为公开 GET 轮询。若同源可考虑 postMessage 扩展(未默认实现)。

Q:能否继续用已有 thread_id 嵌入?
A:可以:#/embed/chats/{thread_id}?sessionId=...,可选再带 message 追加一轮;登记会在已知 id 上 upsert。


10. 变更记录

日期 说明
2026-06-02 初版:iframe 嵌入路由、sessionId 映射表、公开登记与状态 API