deerflow-code/frontend-web/docs/embed-iframe-chat.md
2026-09-07 18:24:55 +08:00

14 KiB
Raw Permalink 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://47.88.25.99:7001/)。

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 获取生成结果(父页面使用)

问答结束后(建议先确认 finished === true),拉取本次模型输出:

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

尚未登记:

{
  "session_id": "parent-generated-uuid",
  "registered": false,
  "thread_id": null,
  "finished": false,
  "thread_status": "pending",
  "has_active_run": false,
  "latest_run_status": null,
  "messages": [],
  "generated_text": "",
  "generated_files": []
}

已登记且已有内容:

{
  "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",
  "messages": [
    { "type": "human", "content": "请总结要点", "id": "..." },
    { "type": "ai", "content": "根据分析,要点如下:...", "id": "..." }
  ],
  "generated_text": "根据分析,要点如下:...",
  "generated_files": [
    "/mnt/user-data/outputs/summary.md",
    "/mnt/user-data/outputs/chart.png"
  ]
}

结果字段说明

字段 含义
messages 完整消息列表(与 checkpoint 中序列化结构一致,含 human / ai / tool 等)
generated_text 所有 ai / assistant 消息正文按顺序拼接(段落间空一行)
generated_files 本次会话产物的虚拟路径列表(与 UI 产物栏 artifacts 一致)

未结束时也可调用,会返回当前已有内容;finished 仍为 false 表示可能还会继续生成。

4.4 按 thread_id 查询(可选)

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

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

状态接口响应不含 messages / generated_text / generated_files;结果接口字段与 §4.3 相同(无 session_id / registered)。

4.5 前端封装

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

// 父页面(或你们自己的脚本)
const status = await getEmbedSessionStatus(sessionId);
if (status.registered && status.finished) {
  const result = await getEmbedSessionResult(sessionId);
  console.log(result.generated_text, result.generated_files);
}

// 仅 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://47.88.25.99:7001/"; // 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 poll = await pollUntilFinished(sessionId);
    if (poll.done) {
      clearInterval(timer);
      const output = await fetch(
        `${API}/api/public/embed/sessions/${encodeURIComponent(sessionId)}/result`,
        { credentials: "omit" },
      ).then((r) => r.json());
      console.log("问答已结束", output.generated_text, output.generated_files);
    }
  } catch (e) {
    console.error(e);
  }
}, 2000);

5.3 跨域注意

父页面从浏览器 fetch Gateway(如 http://47.88.25.99:7001/)时,Origin 必须是后端 CORS 白名单里的完整地址(含协议、主机、端口),与 API 地址是否写 localhost / 127.0.0.1 无关。

本地开发(前端默认 5174)请在 Gateway 环境变量中配置:

# offline-backend-20260512/.env 或启动脚本注入
GATEWAY_CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174,http://localhost:5173,http://127.0.0.1:5173

修改后重启 Gateway(make dev / make gateway)。若父页面部署在其它域名,把该域名的 origin 一并加入逗号分隔列表。

临时调试(勿用于公网生产):

GATEWAY_CORS_ALLOW_ALL=1

常见报错:No 'Access-Control-Allow-Origin' header → 当前页面的 origin 未出现在 GATEWAY_CORS_ORIGINS 中。

5.4 其它部署注意


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
2026-06-02 新增 GET /sessions/{session_id}/result:返回 messages、generated_text、generated_files