deerflow-code/frontend-web/docs/angular-智能体对话对接文档.md
2026-09-07 18:24:55 +08:00

62 KiB
Raw Permalink Blame History

Angular 对接 DeerFlow 智能体对话页

本文说明如何在 其它 Angular 项目 中对接 DeerFlow 智能体对话能力,对标页面:

http://localhost:5173/#/page/workspace/agents/roundtable-structure/chats/new

对应前端路由:#/page/workspace/agents/{agent_id}/chats/{thread_id}
其中 agent_id = roundtable-structure(展示名「结构化抽取」),thread_id = new 表示新建会话。

本文覆盖:登录拿 Token → 列模型/切模型 → 建会话/存聊天记录 → 流式对话 → 思考内容 → 步骤条(含创建脚本/执行脚本/写入文件及全部工具)→ 用户协助卡片 → 消息列表渲染 → 停止/重连/上传/产物。
不包含智能体配置(创建/编辑智能体、技能绑定等)。

路径约定(均相对本仓库根目录 deerflow-server/):

  • 前端:frontend-web/src/…
  • 后端:offline-backend-20260512/backend/…(下文若写 app/、deerflow/,都在这个 backend 目录下)
  • 每节末尾有 参考代码;文末第 20 节是总索引。

0. 先看这一页

对接本质不是「调一个聊天 HTTP」,而是复用本系统的 LangGraph 兼容协议:

能力 走哪条线 Base URL
登录、模型列表、上传、产物、追问建议 Gateway REST {GATEWAY},例如 http://host:8001
会话 / 流式对话 / 取消 LangGraph Platform 兼容 API {GATEWAY}/api

本仓库前端环境变量:

  • VITE_BACKEND_BASE_URL → {GATEWAY}
  • VITE_LANGGRAPH_BASE_URL → {GATEWAY}/api

开发机前端常见端口是 5173 或 5174(Vite 被占用会换端口),Gateway 默认 8001。
下文用:

GATEWAY = http://localhost:8001
LG      = http://localhost:8001/api

所有对话接口都要带:

Authorization: Bearer <access_token>
Content-Type: application/json

用 Bearer 不必再带 CSRF。不要用 cookie 登录(/login/local 只把 JWT 写进 HttpOnly cookie,不适合 Angular 跨域 API 客户端)。

参考代码

说明 路径
前端两个 Base URL frontend-web/src/core/config/index.ts(getBackendBaseURL / getLangGraphBaseURL)
环境变量注入 frontend-web/src/env.ts
LangGraph SDK 客户端(自动带 Bearer) frontend-web/src/core/api/api-client.ts
Gateway fetch 自动带 Authorization frontend-web/src/core/api/fetch-client.ts
CSRF:有 Bearer 则跳过 offline-backend-20260512/backend/app/gateway/csrf_middleware.py
登录接口白名单 offline-backend-20260512/backend/app/gateway/auth_middleware.py

1. 架构与端到端流程

sequenceDiagram
  participant A as Angular
  participant G as Gateway :8001
  participant R as RunManager / Lead Agent

  A->>G: POST /api/v1/auth/login/username
  G-->>A: access_token (JWT, ~7 天)
  A->>G: GET /api/models
  G-->>A: models[]
  A->>G: POST /api/threads  metadata.agent_id=roundtable-structure
  G-->>A: thread_id
  A->>G: POST /api/threads/{id}/runs/stream (SSE)
  G->>R: start_run(assistant_id, context.model_name, ...)
  loop 流式
    R-->>A: event: metadata / messages / values / custom / end
  end
  Note over A: 按消息分组渲染气泡 + 步骤条 + 协助卡片
  A->>G: GET /api/threads/{id}/state  (刷新后还原聊天记录)

roundtable-structure 与其它自定义智能体共用 同一套 Lead Agent 图。
真正选中哪个智能体靠三层同时带上(缺一不可,建议都传):

  1. 请求体 assistant_id = "roundtable-structure"(文件系统/库表里的 id,不是中文名)
  2. context.agent_id = "roundtable-structure"
  3. 会话 metadata.agent_id = "roundtable-structure"(历史列表按此过滤)

参考代码

说明 路径
路由挂载 AgentChatPage frontend-web/src/pages/WorkspaceRoutes.tsx(agents/:agent_id/chats/:thread_id)
对话页:assistantId + context.agent_id frontend-web/src/pages/AgentChatPage.tsx
聊天气泡壳 frontend-web/src/components/workspace/chats/chat-box.tsx
发消息时写入 metadata.agent_id frontend-web/src/core/threads/hooks.ts(onCreated / thread.submit)
非内置 assistant_id → agent_id offline-backend-20260512/backend/app/gateway/services.py(build_run_config)
Lead Agent 工厂 offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py
智能体种子(id / 中文名 / 技能) offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/config.yaml
智能体 SOUL offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md

2. CORS / 跨域(Angular 必读)

Gateway 默认放行的浏览器 Origin 只有:

  • http://localhost:5173 / 5174 / 3000 / 8080(及对应 127.0.0.1)

Angular CLI 默认是 http://localhost:4200,不在白名单里。
部署或本地对接时必须把 Angular 源加进环境变量,例如:

GATEWAY_CORS_ORIGINS=http://localhost:4200,http://127.0.0.1:4200,https://your-angular-host

或开发期临时 GATEWAY_CORS_ALLOW_ALL=1(勿用于生产)。

参考代码:offline-backend-20260512/backend/app/gateway/app.py(搜 GATEWAY_CORS_ORIGINS / local_vite_origins / cors_expose_headers)。回归:offline-backend-20260512/backend/tests/test_taskcop_cors.py。

浏览器跨域读 SSE 还需要暴露这些响应头(Gateway 已配):

  • Content-Location(从中解析 run_id,取消/重连要用)
  • Location、Content-Disposition

请求请带 credentials: include 也可以,但 Bearer 才是主认证。


3. 登录:如何拿到 Token

登录接口 不需要 JWT。前缀:/api/v1/auth。

推荐 Angular 用 用户名登录 或 上游 Token 换票。不要用 /login/local。

3.1 用户名登录(推荐)

POST {GATEWAY}/api/v1/auth/login/username?password=OPTIONAL
Content-Type: application/json

{ "username": "alice" }
  • 若该账号在后台配了「登录口令」,必须带匹配的 ?password=
  • 若开了全局共享口令门(config.yaml → auth_login.username_login.require_password),未配个人口令的账号也要带共享口令
  • 未开密码门且无个人口令时,可省略 password

成功响应:

{
  "access_token": "<jwt>",
  "token_type": "bearer",
  "expires_in": 604800,
  "user_id": "<uuid>",
  "email": "alice@deerflow.local",
  "system_role": "user",
  "needs_setup": false,
  "created": false,
  "username": "alice"
}

expires_in 默认 7 天(604800 秒)。系统 没有 refresh 接口,过期后重新登录。
JWT payload:{ sub: user_id, exp, iat, ver },HS256。改密码会 bump token_version,旧票立刻失效。

后续所有请求:

Authorization: Bearer <access_token>

即 ${token_type} ${access_token}。

校验当前用户:

GET {GATEWAY}/api/v1/auth/me
Authorization: Bearer <jwt>
{ "id": "...", "email": "...", "system_role": "user", "needs_setup": false }

401 → 清本地 token,回到登录。

3.2 上游 Token 换票

适用于已有统一认证、URL 里带 authToken 的场景:

POST {GATEWAY}/api/v1/auth/login/token?token=<upstreamToken>
Content-Type: application/json

响应形状与用户名登录相同。上游时钟偏差可能导致短暂 401,本系统前端会重试最多 3 次、间隔 3 秒;Angular 建议同样处理。

开关:config.yaml → auth_login.token_login.enabled。

3.3 登录失败

detail 可能是字符串、{ code, message }、或校验数组。请按字符串抽取,不要直接 String(object)。

常见:

HTTP 含义
401 口令错误 / 上游 token 无效
403 { code: "PENDING_APPROVAL" } 管理员取消了该账号放行
429 限流

3.4 Angular 存储建议

const AUTH_KEY = 'deerflow.auth';

interface LoginResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
  user_id: string;
  email: string;
  system_role: string;
  username?: string;
}

function authHeader(): HttpHeaders {
  const auth = JSON.parse(localStorage.getItem(AUTH_KEY) || 'null') as LoginResponse | null;
  return new HttpHeaders({
    Authorization: `${auth?.token_type || 'Bearer'} ${auth?.access_token || ''}`,
  });
}

参考代码

说明 路径
前端登录 API frontend-web/src/core/auth/api.ts(loginByUsername / loginByToken)
Token 存取、拼 Bearer frontend-web/src/core/auth/index.ts(getAuthorizationHeaderValue / setStoredAuth)
类型 LoginByUsernameResponse frontend-web/src/core/auth/index.ts
登录页(含 token 换票) frontend-web/src/pages/LoginPage.tsx
后端登录路由 offline-backend-20260512/backend/app/gateway/routers/auth.py(/login/username、/login/token、/me)
JWT 签发 offline-backend-20260512/backend/app/gateway/auth/jwt.py
请求里解析 cookie / Bearer offline-backend-20260512/backend/app/gateway/deps.py(get_current_user)
登录配置(口令门 / token_login) offline-backend-20260512/backend/config.yaml → auth_login;schema:deerflow/config/login_config.py
登录契约说明 offline-backend-20260512/backend/docs/AUTH_LOGIN.md

4. 模型列表与切换

智能体配置不用做;对话时选哪个大模型要做。

4.1 拉模型

GET {GATEWAY}/api/models
Authorization: Bearer <jwt>
{
  "models": [
    {
      "name": "deepseek-chat",
      "model": "deepseek-chat",
      "display_name": "DeepSeek Chat",
      "supports_thinking": true,
      "supports_reasoning_effort": false,
      "supports_vision": false,
      "supports_tool_calling": true,
      "is_local": false,
      "provider": "openai"
    }
  ],
  "token_usage": { "enabled": false }
}

下拉框展示 display_name,提交时传 name(不是 model) 到 context.model_name。

4.2 思考档位(本系统输入框的 flash / thinking / pro / ultra)

每轮 runs/stream 的 context 里带这些开关(后端白名单字段,未列入的会被丢掉):

档位 thinking_enabled is_plan_mode subagent_enabled reasoning_effort 效果
flash false false false — 快答,尽量不思考
thinking true false false low 出思考块
pro true true false medium 思考 + 计划 Todo
ultra true true true high 再加子任务委派

若所选模型 supports_thinking === false,请强制 flash。
内网部分 vLLM/Qwen 只声明了 supports_thinking,关思考时建议额外传 thinking_force_disabled: true,否则可能关不干净。

模型解析顺序(后端):

  1. 本轮 context.model_name
  2. 智能体自己的 config.yaml → model
  3. 系统配置的第一个模型

每轮都可以换模型;换了只影响 下一轮,不会改写历史。

参考代码

说明 路径
前端拉模型 frontend-web/src/core/models/api.ts、frontend-web/src/core/models/types.ts
输入框模型下拉 + 档位 frontend-web/src/components/workspace/input-box.tsx(InputMode、ModelSelector)
档位写入 context frontend-web/src/core/threads/hooks.ts(thread.submit 的 context:thinking_enabled / is_plan_mode / subagent_enabled)
本地记住所选模型 frontend-web/src/core/settings/local.ts(deerflow.thread-model.{threadId})
后端模型列表 offline-backend-20260512/backend/app/gateway/routers/models.py
context 白名单 offline-backend-20260512/backend/app/gateway/services.py(_CONTEXT_CONFIGURABLE_KEYS)
本轮实际用哪个模型 offline-backend-20260512/backend/app/gateway/services.py(_resolve_effective_run_model_name)
创建 ChatModel、关思考 offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py
模型配置源 offline-backend-20260512/backend/config.yaml → models[]

5. 会话(Thread)= 聊天记录的存储单位

聊天记录 不用 Angular 自己建库。Gateway 用 LangGraph checkpointer 存消息,用 threads_meta 存索引(标题、owner、metadata)。

物理目录(了解即可):.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}。

5.1 新建会话

POST {LG}/threads
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "assistant_id": "roundtable-structure",
  "metadata": { "agent_id": "roundtable-structure" }
}
{
  "thread_id": "3f2a…",
  "status": "idle",
  "created_at": "2026-08-18T09:00:00+08:00",
  "updated_at": "…",
  "metadata": { "agent_id": "roundtable-structure" },
  "values": {},
  "interrupts": {}
}

也可以不先 create:runs/stream 带 if_not_exists: "create" 时会自动建。本系统前端就是 第一次发消息时 由 SDK 建 thread,再 PATCH 写入 metadata.agent_id。建议 Angular 显式 create,方便先上传文件、再发问。

5.2 补 metadata(必须,否则历史列表滤不出来)

PATCH {LG}/threads/{thread_id}
{ "metadata": { "agent_id": "roundtable-structure" } }

为 merge,不会整表覆盖。

5.3 历史列表(按智能体过滤)

POST {LG}/threads/search
{
  "metadata": { "agent_id": "roundtable-structure" },
  "limit": 50,
  "offset": 0,
  "query": null,
  "status": null
}

返回 ThreadResponse[]。标题在 values.title(来自库里的 display_name)。
query 按标题模糊搜。系统会话(调度、会商工人线程)默认排除。

条数:

POST {LG}/threads/count
(body 同上,limit/offset 忽略)
→ { "total": 12 }

列表项建议展示:

  1. values.title;没有则用该会话第一条用户消息截断 50 字
  2. updated_at

打开某条:跳到自己的路由并 GET …/state 拉消息。

5.4 打开已有会话(还原聊天记录)

优先:

GET {LG}/threads/{thread_id}/state

values 里至少有:

{
  "title": "抽取方案结构",
  "title_provisional": false,
  "messages": [ /* 见第 8 节 */ ],
  "artifacts": ["/mnt/user-data/outputs/flow.json"],
  "todos": [{ "content": "…", "status": "in_progress" }]
}

也可:

GET {LG}/threads/{thread_id}

带 metadata + 当前 values(含 messages)。

checkpoint 历史(一般不需要):

POST {LG}/threads/{thread_id}/history
{ "limit": 10, "before": null }

只在最新一条 history 上带 messages,避免重复。

5.5 删除会话

两步都做才干净:

DELETE {LG}/threads/{thread_id}

这会删 checkpoint / meta / 磁盘目录。本系统前端 SDK 删除后再打一次 Gateway 同路径,确保本地文件清掉。

5.6 会话状态

status:idle | busy | interrupted | error
正在跑时为 busy。同一会话再发会被 multitask_strategy: "reject" 打回 HTTP 409。

参考代码

说明 路径
线程类型 / context 类型 frontend-web/src/core/threads/types.ts
列表、标题、路由 frontend-web/src/core/threads/utils.ts(titleOfThread / pathOfThread)
分页搜索 frontend-web/src/core/threads/hooks.ts(useThreadsPage)
智能体最近对话 frontend-web/src/components/workspace/agents/agent-recent-chats.tsx
工作区会话列表 UI frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
建/搜/改/删/state/history offline-backend-20260512/backend/app/gateway/routers/threads.py
ThreadResponse / Search 模型 同上文件内 ThreadResponse、ThreadCreateRequest、ThreadSearchRequest
删除时清本地目录 frontend-web/src/core/threads/hooks.ts(delete 后再调 Gateway DELETE)
409 中文文案 offline-backend-20260512/backend/app/gateway/services.py(create_or_reject / RunManager)

6. 发起智能体对话(核心)

POST {LG}/threads/{thread_id}/runs/stream
Authorization: Bearer <jwt>
Content-Type: application/json
Accept: text/event-stream

6.1 请求体(与本系统 AgentChatPage 对齐)

{
  "assistant_id": "roundtable-structure",
  "input": {
    "messages": [
      {
        "type": "human",
        "content": [{ "type": "text", "text": "请根据这份方案抽取流程图" }],
        "additional_kwargs": {
          "client_ts": "2026-08-18T09:00:00.000Z"
        }
      }
    ]
  },
  "context": {
    "agent_id": "roundtable-structure",
    "model_name": "deepseek-chat",
    "thinking_enabled": true,
    "thinking_force_disabled": false,
    "is_plan_mode": false,
    "subagent_enabled": false,
    "reasoning_effort": "low",
    "memory_injection_enabled": true,
    "rag_mode_enabled": false,
    "summarization_enabled": false,
    "thread_id": "<thread_id>"
  },
  "config": { "recursion_limit": 1000 },
  "stream_mode": ["values", "messages-tuple", "updates", "custom"],
  "stream_subgraphs": true,
  "stream_resumable": true,
  "on_disconnect": "cancel",
  "multitask_strategy": "reject",
  "if_not_exists": "create"
}

要点:

  • input.messages 只传本轮新的 human,不要把历史整包再塞一遍(服务端 checkpointer 会拼)
  • content 可以是纯字符串 "你好",也可以是上面的 text block 数组
  • client_ts 可选,前端用来显示提问时间
  • stream_mode 里写 messages-tuple(LangGraph Platform 名);服务端会转成内部 messages 再以 SSE event: messages 吐出
  • on_disconnect: "cancel":浏览器关 SSE 就停服务端;若希望关页后台继续跑,改 "continue"
  • multitask_strategy: "reject":会话已有进行中的 run → 409,文案类似「当前对话还在生成回答…」

响应头:

Content-Type: text/event-stream
Content-Location: /api/threads/{thread_id}/runs/{run_id}

立刻把 run_id 存下来(停止、重连都靠它)。SSE 第一帧 event: metadata 里也会再给一次。

6.2 纯文本以外的用户消息

图片(模型 supports_vision 时):

{
  "type": "human",
  "content": [
    { "type": "text", "text": "看看这张图" },
    { "type": "image_url", "image_url": { "url": "https://…/a.png" } }
  ]
}

附件:先上传(第 15 节),再在 additional_kwargs.files 里带虚拟路径。

协助卡片的回复:不要调 LangGraph Command(resume),当成 普通新一轮 human 消息 再 runs/stream(见第 12 节)。

6.3 停止生成

只关 EventSource 不够,服务端可能还在跑。

POST {LG}/threads/{thread_id}/runs/{run_id}/cancel?action=interrupt&wait=false
  • 202:已受理;wait=true 等到停完返回 204
  • action=interrupt:停下,保留当前 checkpoint(可继续聊)
  • action=rollback:回滚到本轮开始前

列出该会话的 run:

GET {LG}/threads/{thread_id}/runs
GET {LG}/threads/{thread_id}/runs/{run_id}

6.4 断线重连

SSE 帧带 id:。重连时:

GET {LG}/threads/{thread_id}/runs/{run_id}/join
Last-Event-ID: <上一帧 id>

或 POST stream 时带 stream_resumable: true 后由 SDK 自动 join。Gateway 重启后 join 会立刻 event: end,此时改拉 /state 渲染历史即可。

心跳是注释行:: heartbeat,忽略即可。

参考代码

说明 路径
前端 useStream + thread.submit frontend-web/src/core/threads/hooks.ts(useThreadStream、构造 messages / context)
SDK 包装 frontend-web/src/core/api/api-client.ts
stream_mode 规范化 frontend-web/src/core/api/stream-mode.ts
停止:thread.stop / runs.cancel frontend-web/src/core/threads/hooks.ts(stopThreadRun)
输入框停止按钮 frontend-web/src/components/workspace/input-box.tsx
后端 stream / cancel / join offline-backend-20260512/backend/app/gateway/routers/thread_runs.py(RunCreateRequest、stream_run、cancel_run、join_run)
真正起跑 offline-backend-20260512/backend/app/gateway/services.py(start_run)
后台跑图 + 发 SSE offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py(run_agent)
Run 生命周期 offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/manager.py
流式设计说明 offline-backend-20260512/backend/docs/STREAMING.md

7. SSE 事件协议(Angular 必须自己解析 POST 流)

EventSource 只支持 GET,不能用来 POST /runs/stream。
Angular 请用 fetch + ReadableStream(或封装好的 SSE parser)。不要用 HttpClient 的默认 JSON 解析。

7.1 线格式

event: metadata
data: {"run_id":"…","thread_id":"…"}
id: 1

event: messages
data: [{...chunk...}, {...meta...}]
id: 2

event: values
data: {"title":"…","messages":[...],"artifacts":[],"todos":[]}
id: 3

event: custom
data: {"type":"task_running","task_id":"…","message":"…"}
id: 4

event: end
data: null
id: 5

字段顺序:event: → data: → id: → 空行。data 是一行 JSON(ensure_ascii=false,中文直接出)。

7.2 各事件怎么用

event data UI 该做什么
metadata { run_id, thread_id } 保存 run_id
messages [chunk, meta] token 级增量:拼 AI 正文、思考、tool_call 参数
values 完整 state 权威快照:用 messages[] 替换本地列表(标题、todos、artifacts 也在这)
updates { nodeName: writes } 可忽略;或用来知道刚跑完哪个节点
custom 见下表 toast / 子任务状态
end null 结束本轮,关 loading
error 错误对象 展示错误

custom 常见:

{ "type": "task_running", "task_id": "…", "message": "正在检索…" }
{ "type": "llm_retry", "message": "模型调用失败,正在重试…" }
{ "type": "context_compacting", "message": "正在压缩上下文…" }

后两个弹 toast 即可,不要当消息气泡。

7.3 event: messages 的 chunk(最重要)

data 是二元组:

[
  {
    "type": "AIMessageChunk",
    "id": "run-abc-0",
    "content": "正在",
    "additional_kwargs": {
      "reasoning_content": "用户想抽取结构,我先读文件…"
    },
    "tool_call_chunks": [
      {
        "index": 0,
        "id": "call_xxx",
        "name": "bash",
        "args": "{\"description\": \"创建脚本\""
      }
    ]
  },
  {
    "langgraph_node": "model",
    "ls_provider": "…",
    "ls_model_name": "…"
  }
]

合并规则(按 chunk.id 分桶):

  1. 正文:把每次 content(字符串)往后拼;如果是 block 数组,只拼 type==="text" 的 text
  2. 思考:additional_kwargs.reasoning_content 一般是 累计全文(以最新一帧为准);若是 delta 则拼接。同时要从正文里剥 <think>…</think>(见第 10 节)
  3. 工具调用:按 tool_call_chunks[].index 拼 name / args(args 是 JSON 字符串碎片,拼完再 JSON.parse)
  4. 工具结果:会出现 type: "tool" / ToolMessage,tool_call_id 对上上面的 id,name 为工具名,content 为结果

values.messages 到来后,用快照覆盖增量状态,避免拼错。

7.4 Angular 解析骨架

async function streamRun(
  lgBase: string,
  threadId: string,
  body: unknown,
  token: string,
  onEvent: (event: string, data: unknown) => void,
): Promise<string | undefined> {
  const res = await fetch(`${lgBase}/threads/${threadId}/runs/stream`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      Accept: 'text/event-stream',
    },
    body: JSON.stringify(body),
  });
  if (res.status === 409) throw new Error('当前对话还在生成回答,请稍候再发送');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const location = res.headers.get('Content-Location') || '';
  const runId = location.split('/runs/')[1];

  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buf = '';
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    const parts = buf.split('\n\n');
    buf = parts.pop() || '';
    for (const block of parts) {
      if (block.startsWith(':')) continue; // heartbeat
      let event = 'message';
      let data = '';
      for (const line of block.split('\n')) {
        if (line.startsWith('event:')) event = line.slice(6).trim();
        else if (line.startsWith('data:')) data += line.slice(5).trim();
      }
      if (!data) continue;
      onEvent(event, data === 'null' ? null : JSON.parse(data));
    }
  }
  return runId;
}

参考代码

说明 路径
SSE 组帧(event: / data: / id:) offline-backend-20260512/backend/app/gateway/services.py(format_sse、sse_consumer)
心跳 / 订阅 offline-backend-20260512/backend/packages/harness/deerflow/runtime/stream_bridge.py
首帧 event: metadata offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py(bridge.publish(..., "metadata", {run_id, thread_id}))
messages-tuple → 内部 messages 同上 run_agent;SSE 事件名 _lg_mode_to_sse_event
chunk 序列化成 [chunk, meta] offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py(serialize_messages_tuple / serialize_channel_values)
前端消费:onCustomEvent / onUpdateEvent frontend-web/src/core/threads/hooks.ts
协议说明 offline-backend-20260512/backend/docs/STREAMING.md

8. 消息数据结构(历史 + 实时共用)

values.messages[] 每条大致如下(LangChain 序列化后):

{
  "type": "human | ai | tool",
  "id": "uuid-or-run-id",
  "name": "ask_clarification | bash | …",
  "content": "字符串 或 content block 数组",
  "tool_calls": [
    { "id": "call_xxx", "name": "bash", "args": { "description": "执行脚本", "command": "python3 a.py" } }
  ],
  "tool_call_id": "call_xxx",
  "additional_kwargs": {
    "reasoning_content": "…",
    "clarification": { },
    "files": [ ],
    "client_ts": "…",
    "hide_from_ui": false
  }
}

8.1 content 形态

形态 含义
"一段文本" 最常见
[{ "type": "text", "text": "…" }] 多段文本
[{ "type": "image_url", "image_url": { "url": "…" } }] 用户图 / 模型回图
[{ "type": "thinking", "thinking": "…" }] Anthropic 风格思考
[{ "type": "json", "json": { } }] 工具结构化结果

8.2 必须隐藏、不要渲染的消息

  • additional_kwargs.hide_from_ui === true(摘要压缩等)
  • name === "summary"
  • name === "todo_reminder"
  • type === "system"(一般不会出现在 UI 列表)

8.3 角色怎么画

type 默认 UI
human 右侧用户气泡,Markdown 可关,展示附件 chips
ai 且无 tool_calls、有正文 左侧助手最终回答(Markdown)
ai 有 tool_calls 或仅有思考 不单独当最终气泡,进入步骤条
tool 几乎不单独成气泡,并进步骤条 / 协助卡片

参考代码

说明 路径
分组、抽文本、抽思考、隐藏消息 frontend-web/src/core/messages/utils.ts(groupMessages / extractContentFromMessage / extractReasoningContentFromMessage / isHiddenFromUIMessage)
检索结果识别 frontend-web/src/core/messages/search-results.ts
序列化后的 message dict offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py

9. 消息列表如何分组渲染

本系统 groupMessages() 的规则,Angular 请原样移植,否则步骤条和协助卡片会对不齐。

从前往后扫 messages(已过滤隐藏消息):

  1. human → 一组 human
  2. ai 且 tool_calls 含 present_files → assistant:present-files(正文 + 文件卡片)
  3. ai 且 tool_calls 含 task → assistant:subagent(子任务卡)
  4. ai 有思考或有其它 tool_calls → 并入(或新建)assistant:processing(步骤条)
  5. 同一条 ai 既有思考又有最终正文、且没有 tool_calls → processing 一组 再 出一组 assistant 气泡
  6. tool 且 name === ask_clarification → 先塞进当前 processing,再单独开 assistant:clarification
  7. 其它 tool → 塞进当前 processing(用 tool_call_id 填步骤的 result)

渲染顺序示例(一轮典型技能执行):

[用户] 请抽取结构
[步骤条]
  思考
  读取文件  SKILL.md
  创建脚本
  执行脚本
  写入文件  /mnt/user-data/outputs/flow.json
  展示文件
[助手] 已生成 flow-json,可在右侧预览。

若中途问用户:

[步骤条] … 需要你的协助
[协助卡片] 需要你的确认
(等待用户下一轮 human)

9.1 用户气泡

  • 文本:extractContentFromMessage,去掉 <uploaded_files>…</uploaded_files> 这类内部标记
  • 附件:additional_kwargs.files[] → { filename, size, path, status }
  • 图片 block → <img>
  • 可选:编辑/删除(删除要改 checkpoint,对接初期可不做)

9.2 助手最终回答

  • Markdown(GFM 表格/代码块)
  • 流式时按 token 追加;values 快照到达后用完整 content 替换
  • 正文里的 <think> 必须剥掉(见第 10 节)
  • 引用:技能检索结果会在步骤里展开,正文里可能有 [n] 引用角标
  • 复制 / 导出 Word 可选

9.3 流式指示

busy 且还没有最终 assistant 气泡时,在列表底部显示跳动的「正在生成」。
上传文件未完成时可插一条 mock AI:additional_kwargs.element === "task",文案「文件上传中,请稍候...」。

参考代码

说明 路径
列表入口、分组渲染 frontend-web/src/components/workspace/messages/message-list.tsx
单条用户/助手气泡 frontend-web/src/components/workspace/messages/message-list-item.tsx
Markdown frontend-web/src/components/workspace/messages/markdown-content.tsx
气泡/会话滚动容器 frontend-web/src/components/ai-elements/message.tsx、conversation.tsx
跳动「正在生成」 frontend-web/src/components/workspace/streaming-indicator.tsx
技能来源展示 frontend-web/src/core/skill-display/runtime.ts
导出 frontend-web/src/core/threads/export.ts;Word:frontend-web/src/components/workspace/messages/download-word-button.tsx
删除消息 frontend-web/src/components/workspace/messages/delete-message-button.tsx
问题跳转 frontend-web/src/components/workspace/question-navigator.tsx

10. 思考内容如何对接

思考 没有 独立 SSE 事件名,混在 AI 消息里。按优先级取:

  1. additional_kwargs.reasoning_content(字符串,多数内网模型走这条)
  2. content block { type: "thinking", thinking: "…" }
  3. 正文里的 <think>…</think>(含流式 未闭合 的 <think>)

10.1 剥离算法(请直接用)

const THINK_RE = /<think>\s*([\s\S]*?)\s*<\/think>/g;
const THINK_OPEN = '<think>';

function splitThink(content: string): { answer: string; thinking: string | null } {
  const parts: string[] = [];
  let working = content.replace(THINK_RE, (_, t: string) => {
    const s = t.trim();
    if (s) parts.push(s);
    return '';
  });
  const i = working.indexOf(THINK_OPEN);
  if (i !== -1 && !working.slice(i).includes('</think>')) {
    const partial = working.slice(i + THINK_OPEN.length).trim();
    if (partial) parts.push(partial);
    working = working.slice(0, i);
  }
  return { answer: working.trim(), thinking: parts.length ? parts.join('\n\n') : null };
}

展示用 answer,思考区用 reasoning_content || thinking。

10.2 UI 约定(与现网一致)

  • 步骤条里:思考是一种 step,标题 「思考」,可折叠,灯泡图标
  • 只有思考、还没有工具、还没有最终正文时:单独一块可折叠「思考」,流式时默认展开,结束后可自动收起
  • 历史刷新后思考仍在(写在 checkpoint 的 AI 消息上),不要丢
  • 不要把思考当作用户可见的最终答案

thinking_enabled: false 时模型不应再吐思考;若仍出现 <think>,继续剥掉以免露标签。

参考代码

说明 路径
抽思考 / 剥 <think>(含未闭合) frontend-web/src/core/messages/utils.ts(extractReasoningContentFromMessage、splitInlineReasoning)
步骤条里的思考步 frontend-web/src/components/workspace/messages/message-group.tsx(convertToSteps 的 reasoning)
独立思考折叠块 frontend-web/src/components/ai-elements/reasoning.tsx
文案「思考」 frontend-web/src/core/i18n/locales/zh-CN.ts(t.common.thinking)
模型思考流字段 offline-backend-20260512/backend/packages/harness/deerflow/models/vllm_provider.py
关思考注入 extra_body offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py

11. 步骤条(Chain of Thought)— 请做全

步骤条只渲染 assistant:processing 组。内部只有两类 step:

  • reasoning:思考文本
  • toolCall:一次工具调用(task 子任务除外,走独立卡片)

11.1 从消息抽出 steps

对组内每条 ai:

  1. 若有思考 → push { type: "reasoning", reasoning }
  2. 遍历 tool_calls,跳过 name === "task"
  3. 用 tool_call.id 在组内找对应 type==="tool" 的消息,作为 result
  4. result.content 若是 JSON 字符串就 parse

UI:

  • 默认只展示 最后一个工具步骤 + 其后的思考
  • 上方折叠:「查看其他 N 个步骤」/「隐藏步骤」
  • 进行中:该步 status=active(转圈);有 result 后 complete(对勾)
  • 工具参数还在流式拼接时,label 用已有 args.description 或占位「正在调用工具…」

11.2 步骤标题怎么来(「创建脚本 / 执行脚本 / 写入文件」在这里)

系统没有名为 create_script / execute_script 的工具。
模型调用沙箱工具时 必须先填 args.description(短中文说明),步骤条 优先展示 description。所以你会看到:

用户看到的步骤 实际 tool_calls[].name args
创建脚本 bash { description: "创建脚本", command: "cat > /tmp/a.py <<'EOF' …" }
执行脚本 bash { description: "执行脚本", command: "python3 /tmp/a.py" }
写入文件 write_file 或 str_replace { description: "写入文件", path: "/mnt/user-data/outputs/flow.json", content: "…" }
读取技能说明 read_file { description: "读取技能说明", path: "/mnt/skills/public/…/SKILL.md" }

description 是模型写的,文案不固定。没有 description 时再用下表中文底稿。

11.3 全量工具 → 步骤条映射

与现网 message-group.tsx + zh-CN.ts toolCalls 对齐。未知 MCP/技能脚本一律走最后一行。

name 默认中文标题 图标建议 展开区
(reasoning step) 思考 灯泡 Markdown/纯文本思考
web_search 知识库检索 "{query}";无 query 则为「知识库检索」 搜索 结果列表(title/snippet/url),可折叠「N 条检索结果」
任意工具但 result 长得像检索 { results: [...] } 搜索 “{query}” 或「搜索相关信息」 搜索 同上
技能展示/RAG 引用模式 「检索参考来源」或「获取 N 条结果」 搜索 来源卡片(title/snippet/原文)
image_search 搜索相关图片 “{query}” 搜索 缩略图网格,点开 source_url
web_fetch 查看网页 地球 链接,标题从返回 Markdown 的 # title 抽
ls args.description 或 列出文件夹 文件夹 path chip
read_file args.description 或 读取文件 书本 path chip
write_file args.description 或 写入文件 笔记本 path;点击打开产物预览
str_replace 同上(默认也叫写入文件) 笔记本 path
bash 必须用 args.description;没有则 执行命令 终端 展示 command 的 bash 代码块(不一定展示 stdout,stdout 在 tool result 里)
glob args.description 或 使用 “glob” 工具 扳手 可选 path/pattern
grep args.description 或 使用 “grep” 工具 扳手 pattern / path
browser_fetch_page args.description 或 使用 “browser_fetch_page” 工具 扳手 url
browser_act args.description 或 使用 “browser_act” 工具 扳手 动作说明
ask_clarification 需要你的协助 问号 空(真正交互在卡片)
write_todos 更新 To-do 列表 清单 空;完整列表见输入框上方 Todo 面板
present_files 一般不进步骤条,进「展示文件」组 文件 见 13.1
update_artifact args.title 或「生成代码」/「生成文档」 笔 「代码文件」或「Markdown 文档」
view_image args.description 或 使用 “view_image” 工具 扳手 图片路径
skill_list args.description 或 使用 “skill_list” 工具 扳手 —
skill_view 读取某个技能 扳手 技能名
search_skills 检索可用技能 扳手 关键词
memory / hindsight_* 记忆读写 扳手 —
deep_research_progress args.label 或「正在撰写研究报告」 转圈/对勾 detail + 可折叠「模型思考」;status!=="complete" 为进行中
task 不要画在步骤条 — 见 13.2 子任务卡
其它 MCP / 自定义工具 args.description 或 使用 “{name}” 工具 扳手 有 path/url 可展示

roundtable-structure 常走技能 knowledge-base-ingest,因此一轮里会密集出现:read_file(SKILL.md)→ bash(创建/执行脚本)→ write_file(产物)→ present_files。把 bash.description 原样显示,就能覆盖「创建脚本」「执行脚本」。

11.4 工具参数字段(沙箱类)

所有沙箱工具的 第一个参数都是 description:

interface BashArgs { description: string; command: string }
interface LsArgs { description: string; path: string }
interface ReadFileArgs { description: string; path: string }
interface WriteFileArgs { description: string; path: string; content: string }
interface StrReplaceArgs { description: string; path: string; old_str: string; new_str: string }
interface GlobArgs { description: string; pattern: string; path?: string }
interface GrepArgs { description: string; pattern: string; path?: string }
interface WebSearchArgs { query: string }
interface WebFetchArgs { url: string }

write_file 流式时 content 会变长,可做实时预览(现网点击步骤打开右侧产物面板)。

11.5 检索结果 result 形状(web_search / 技能)

常见:

{
  "success": true,
  "query": "台湾滨海防卫",
  "count": 10,
  "results": [
    {
      "title": "…",
      "content_preview": "…",
      "url": "https://…",
      "recUuid": "…",
      "score": 0.8
    }
  ]
}

字段名不统一(content / page_content / snippet / m_title / name),展示时做并集兜底。

参考代码

说明 路径
步骤条主实现(必看) frontend-web/src/components/workspace/messages/message-group.tsx(convertToSteps、按 name 分支、ArtifactWriteToolCall)
ChainOfThought 组件 frontend-web/src/components/ai-elements/chain-of-thought.tsx
中文底稿 frontend-web/src/core/i18n/locales/zh-CN.ts → toolCalls;类型 frontend-web/src/core/i18n/locales/types.ts
工具一句话说明 frontend-web/src/core/tools/utils.ts(explainToolCall)
检索结果卡片 frontend-web/src/core/messages/search-results.ts
写文件实时预览 frontend-web/src/core/artifacts/streaming-write-preview.ts
bash / ls / read / write / glob / grep offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py(参数都要求先填 description)
工具装配(含 MCP、内置) offline-backend-20260512/backend/packages/harness/deerflow/tools/tools.py
present_files offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/present_file_tool.py
skill_list / skill_view / search_skills offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/skill_tools.py
启用哪些工具 offline-backend-20260512/backend/config.yaml → tools[]
会商页步骤条(对照用,对话页以 message-group 为准) frontend-web/src/roundtable-planning/lib/step-display.tsx

12. 用户协助卡片(ask_clarification)

当模型信息不够、有多种做法、或要确认风险时,会调用工具 ask_clarification。
ClarificationMiddleware 会截住该工具,写入 ToolMessage,并 goto=END 结束本轮。
这 不是 LangGraph interrupt() / Command(resume)。用户点选项后,发 新的 human 消息 即可,智能体会带着选择继续。

12.1 如何识别

message.type === "tool" && message.name === "ask_clarification"

结构化数据在:

{
  "additional_kwargs": {
    "clarification": {
      "question": "流程图层级按哪套业务?",
      "clarification_type": "approach_choice",
      "context": "报告里同时出现了任务链和目的链。",
      "options": [
        { "id": "option-0", "label": "任务→目的→行为体", "description": "可选" },
        { "id": "option-1", "label": "按报告原标题层级" }
      ],
      "allow_custom": true,
      "allow_multiple": false
    }
  },
  "content": "(Markdown 兜底文案,无结构化数据时用这个渲染)"
}

clarification_type:

  • missing_info 缺信息
  • ambiguous_requirement 需求有歧义
  • approach_choice 方案选择
  • risk_confirmation 风险确认
  • suggestion 建议确认

同一轮可以有 多张 独立卡片(多个 tool call)。只统计 上一条 human 之后 的 clarification 为「待答」。

12.2 UI(与现网文案)

  • 标题:需要你的确认;多张时显示 1/N
  • 展示 context、question
  • 选项做成按钮
  • allow_multiple !== true:单选。仅一张待答卡时,点击即提交;多张时只记录选择,点最后一张上的「全部发送」
  • allow_multiple === true:多选 + 必须点提交
  • allow_custom !== false(后端恒为 true):额外文本框
  • 按钮:「发送回答」/ 多张时「全部发送」
  • 历史里已答过的卡:禁用,只读回显

步骤条里对应一步标题仍是「需要你的协助」。

12.3 提交内容格式(必须按这个拼,模型才认)

单张:

我选择:任务→目的→行为体
我选择:A、B;补充:用户打的字
(只有自定义)用户打的字

多张:

问题1「……」:我选择:A
问题2「……」:补充:……

然后:

POST …/runs/stream
input.messages = [{ type: "human", content: "<上面拼好的文本>" }]

不要调 /state 的 resume,也不要自己造 ToolMessage。

参考代码

说明 路径
卡片 UI、拼「我选择:」、多卡合并提交 frontend-web/src/components/workspace/messages/message-list.tsx(ClarificationCard、handleSubmitAllClarifications)
识别 tool 消息 frontend-web/src/core/messages/utils.ts(isClarificationToolMessage)
工具定义 offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_tool.py
截住工具、写 additional_kwargs.clarification、goto END offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py(_build_payload)
allow_multiple 解析 offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py
提交后又走同一套 sendMessage frontend-web/src/pages/AgentChatPage.tsx、frontend-web/src/core/threads/hooks.ts(不是 Command resume)

13. 其它卡片与面板

13.1 展示文件 present_files

AI tool_calls 含 name: "present_files",args.filepaths: string[](必须是 /mnt/user-data/outputs/…)。

UI:Markdown 摘要 + 文件卡片。点击后:

GET {GATEWAY}/api/threads/{thread_id}/artifacts/{path}

path 例:mnt/user-data/outputs/flow.json(虚拟前缀,不要用 Windows 盘符)。
?download=true 强制下载。HTML/SVG 会当附件,降低 XSS。

列表:

GET {GATEWAY}/api/threads/{thread_id}/artifacts
→ { "thread_id", "files": [{ path, name, size_bytes, mime_type, modified_at }] }

state 里的 artifacts: string[] 是虚拟路径列表,可与卡片同步。

13.2 子任务 task(ultra 档)

tool_calls[].name === "task",args 含 description / prompt / subagent_type。
组类型 assistant:subagent,标题「执行 N 个子任务」(N>1 时「并行执行」)。
custom 事件 task_running 更新进行中文案。终态看对应 tool 结果。

roundtable-structure 默认不必开 ultra;对接时可先不做,遇到 task 当普通扳手步骤也行。

13.3 Todo 面板(pro / ultra)

write_todos 出现在步骤条;完整列表在 values.todos:

interface Todo { content?: string; status?: 'pending' | 'in_progress' | 'completed' }

现网浮在输入框上方。一轮只应有一个 in_progress。

13.4 创建智能体 / 写作审批卡

setup_agent / setup_writing 带 approval kwargs。普通 roundtable-structure 对话 不会出现,可忽略。
Deep Research 的 setup_research_approval 同理。

参考代码

说明 路径
present_files 分组渲染 frontend-web/src/components/workspace/messages/message-list.tsx(assistant:present-files)
产物 API offline-backend-20260512/backend/app/gateway/routers/artifacts.py
产物前端 frontend-web/src/components/workspace/artifacts/(如 artifact-file-detail.tsx)
子任务卡 frontend-web/src/components/workspace/messages/subtask-card.tsx
task 工具 / 子代理 offline-backend-20260512/backend/packages/harness/deerflow/subagents/
Todo 面板 frontend-web/src/components/workspace/todo-list.tsx;类型 frontend-web/src/core/todos/types.ts
write_todos 中间件装配 offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py(_build_middlewares)

14. 标题、追问、错误态

14.1 自动标题

第一轮用户消息到达后,TitleMiddleware 先用问题字面做 临时标题(title_provisional: true),再异步 LLM 生成最终标题。
听 event: values 的 title 即可,刷新列表。

14.2 追问建议(一轮结束后)

POST {GATEWAY}/api/threads/{thread_id}/suggestions
{
  "messages": [
    { "role": "user", "content": "…" },
    { "role": "assistant", "content": "…" }
  ],
  "n": 3,
  "model_name": "deepseek-chat"
}
{ "suggestions": ["是否导出 JSON?", "按另一套层级再抽一次"] }

点建议 = 再发一条 human。失败时返回空数组,不要报错挡 UI。

14.3 错误与限流

情况 表现 UI
会话已在生成 HTTP 409 toast:「当前对话还在生成回答…」,不要红框
全局并发打满 HTTP 429 toast 提示稍后再试
模型失败重试 custom.llm_retry 轻提示
上下文压缩 custom.context_compacting 「正在压缩上下文…」
会话不属于当前用户 403/404 回到新建

参考代码

说明 路径
标题展示 frontend-web/src/components/workspace/thread-title.tsx
列表用 title frontend-web/src/core/threads/utils.ts(titleOfThread)
临时标题 + LLM 标题 offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py
追问 API 调用 frontend-web/src/components/workspace/input-box.tsx(/api/threads/${threadId}/suggestions)
追问后端 offline-backend-20260512/backend/app/gateway/routers/suggestions.py
409 toast frontend-web/src/core/threads/hooks.ts(isActiveRunConflictError / onError)
压缩 toast offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py(custom.context_compacting)

15. 文件上传(用户侧)

先有 thread_id(可先 create 空会话)。

POST {GATEWAY}/api/threads/{thread_id}/uploads
Authorization: Bearer <jwt>
Content-Type: multipart/form-data

字段名 files,可多文件。默认上限约 10 个、单文件 50MB、合计 100MB。
PDF/PPT/Excel/Word 会转成 Markdown 一并放入 uploads。

{
  "success": true,
  "message": "Successfully uploaded 1 file(s)",
  "files": [
    {
      "filename": "report.docx",
      "virtual_path": "/mnt/user-data/uploads/report.docx",
      "markdown_virtual_path": "/mnt/user-data/uploads/report.md"
    }
  ],
  "skipped_files": []
}

发消息时:

"additional_kwargs": {
  "files": [
    { "filename": "report.md", "path": "/mnt/user-data/uploads/report.md", "status": "uploaded" }
  ]
}

限额:GET /api/threads/{thread_id}/uploads/limits。
列表/删除:GET …/uploads/list,DELETE …/uploads/{filename}。

参考代码

说明 路径
前端上传 frontend-web/src/components/workspace/input-box.tsx;预览 frontend-web/src/components/workspace/messages/uploaded-file-preview-dialog.tsx
后端上传路由 offline-backend-20260512/backend/app/gateway/routers/uploads.py
目录与虚拟路径 offline-backend-20260512/backend/packages/harness/deerflow/uploads/manager.py
文档转 Markdown offline-backend-20260512/backend/packages/harness/deerflow/utils/file_conversion.py
说明文档 offline-backend-20260512/backend/docs/FILE_UPLOAD.md

16. 推荐的页面结构(对标 AgentChatPage)

不必做智能体配置侧栏。最小工作台:

┌─ 左:会话列表(threads/search by agent_id)─┬─ 中:消息列表 ─┬─ 右:产物预览 ─┐
│  新建对话                                      │  标题(values.title)│  artifacts     │
│  每条 title + 时间                             │  用户/步骤条/卡片/回答│                │
│                                                │  Todo(可选)        │                │
│                                                │  输入框:模型+档位+停止│                │
└────────────────────────────────────────────────┴─────────────────────┴────────────────┘

输入框建议:

  • 模型 Select(GET /api/models)
  • 档位:快速 / 思考 / 规划(对应 flash / thinking / pro);ultra 可隐藏
  • 发送、停止(streaming 时)
  • 可选:上传、追问 chips

roundtable-structure 是「结构化抽取」智能体:给它方案报告或席位交付,产出 flow-json 流程图数据,常伴随写文件 + 展示文件。不要当成多智能体会商编排页(/roundtable 是另一套 API)。

参考代码

说明 路径
对话页整页 frontend-web/src/pages/AgentChatPage.tsx
路由 frontend-web/src/pages/WorkspaceRoutes.tsx
ChatBox(标题栏 + 列表 + 输入) frontend-web/src/components/workspace/chats/chat-box.tsx
输入框 frontend-web/src/components/workspace/input-box.tsx
左栏最近会话 frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx、agent-recent-chats.tsx
欢迎/推荐问题 frontend-web/src/components/workspace/agent-welcome.tsx、agents/agent-question-chips.tsx
智能体种子 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/
会商编排(不要和本页搞混) frontend-web/src/roundtable-planning/、offline-backend-20260512/backend/app/gateway/routers/multi_agent.py
iframe 嵌现网 UI(备选) frontend-web/docs/embed-iframe-chat.md

17. 最小对接顺序(验收清单)

  1. POST /api/v1/auth/login/username → 存 JWT
  2. CORS 已包含 Angular Origin
  3. GET /api/models → 下拉,选中项的 name 写入 context.model_name
  4. POST /api/threads + metadata.agent_id=roundtable-structure
  5. POST /api/threads/{id}/runs/stream,assistant_id 与 context.agent_id 都等于 roundtable-structure
  6. 解析 SSE:metadata → messages 增量 → values 覆盖 → end
  7. 消息分组:用户气泡 / 步骤条 / 协助卡片 / 最终 Markdown
  8. 步骤条至少覆盖:bash(description=创建脚本/执行脚本)、write_file(写入文件)、read_file、ls、web_search、未知工具
  9. 思考:reasoning_content + <think> 剥离
  10. 协助卡片:选项 → 拼「我选择:」→ 新一轮 human
  11. POST /threads/search 还原列表;GET /state 还原消息(含思考与步骤)
  12. 停止:POST …/runs/{run_id}/cancel
  13. 409 toast
  14. present_files → GET …/artifacts/{path}

18. 接口速查

Base:{GATEWAY} = http://host:8001,LangGraph {LG} = {GATEWAY}/api。
除登录外均需 Authorization: Bearer。

方法 路径 用途
POST /api/v1/auth/login/username 用户名登录
POST /api/v1/auth/login/token?token= 上游 token 换票
GET /api/v1/auth/me 当前用户
GET /api/models 模型列表
POST /api/threads 建会话
POST /api/threads/search 历史列表
POST /api/threads/count 历史条数
GET /api/threads/{id} 会话摘要 + values
GET /api/threads/{id}/state 最新 checkpoint
PATCH /api/threads/{id} 改 metadata
DELETE /api/threads/{id} 删除会话
POST /api/threads/{id}/history checkpoint 历史
POST /api/threads/{id}/runs/stream 流式对话
POST /api/threads/{id}/runs/{rid}/cancel 停止
GET /api/threads/{id}/runs run 列表
GET /api/threads/{id}/runs/{rid}/join 重连 SSE
POST /api/threads/{id}/uploads 上传
GET /api/threads/{id}/artifacts 产物列表
GET /api/threads/{id}/artifacts/{path} 读产物
POST /api/threads/{id}/suggestions 追问

可选简化入口(无登录、不落聊天记录、没有步骤条协议,不建议用来复刻本页):

  • POST /api/open/chat / /api/open/chat/stream(agent_id 或中文 agent_name)

可选 iframe 嵌现网 UI(不自己画步骤条):见 frontend-web/docs/embed-iframe-chat.md。那是通用工作台 /embed/chats,不是 agents/roundtable-structure 路由;若必须嵌本页,可用:

{FRONTEND}/#/page/workspace/agents/roundtable-structure/chats/new?embed=1

仍要先登录;iframe 跨域时走 ?embed=1 可隐藏外壳。要完全自绘 UI,请走本文 REST + SSE。


19. Angular 工程注意

  1. POST SSE 用 fetch,不要 HttpClient.get + EventSource
  2. run_id 从 Content-Location 或 event: metadata 取;取消必须打 cancel
  3. assistant_id 用 roundtable-structure,不要传「结构化抽取」
  4. 历史过滤靠 metadata.agent_id,第一轮后务必 PATCH
  5. 同一 thread 未 end 不要再发,否则 409
  6. JWT 约 7 天,无 refresh
  7. 把 localhost:4200 加入 GATEWAY_CORS_ORIGINS
  8. 可用 npm 包 @langchain/langgraph-sdk 的 Client(apiUrl: LG, defaultHeaders: { Authorization })少写 SSE 细节;UI 仍要自己做分组/步骤条/卡片
  9. 不要把思考、tool 结果、协助卡当成三条普通聊天记录平铺,观感会和现网差很多
  10. 步骤条 label 永远优先 args.description,这样才能显示「创建脚本」「执行脚本」「写入文件」

20. 参考实现总索引

路径均相对仓库根 deerflow-server/。后端根目录 = offline-backend-20260512/backend/。
各节正文末尾也有对应表;这里按主题汇总,加粗的是对接时优先打开的文件。

20.1 前端:页面与 UI

主题 路径
智能体对话页 frontend-web/src/pages/AgentChatPage.tsx
路由 agents/:agent_id/chats/:thread_id frontend-web/src/pages/WorkspaceRoutes.tsx
对话壳(标题 / 列表 / 输入 / 产物入口) frontend-web/src/components/workspace/chats/chat-box.tsx
消息列表 + 协助卡片 frontend-web/src/components/workspace/messages/message-list.tsx
用户/助手气泡 frontend-web/src/components/workspace/messages/message-list-item.tsx
步骤条(创建脚本/执行脚本/写入文件等) frontend-web/src/components/workspace/messages/message-group.tsx
Markdown frontend-web/src/components/workspace/messages/markdown-content.tsx
子任务卡 frontend-web/src/components/workspace/messages/subtask-card.tsx
思考折叠块 frontend-web/src/components/ai-elements/reasoning.tsx
ChainOfThought 控件 frontend-web/src/components/ai-elements/chain-of-thought.tsx
输入框(模型、档位、发送、停止、追问、上传) frontend-web/src/components/workspace/input-box.tsx
Todo 条 frontend-web/src/components/workspace/todo-list.tsx
会话标题 frontend-web/src/components/workspace/thread-title.tsx
正在生成 frontend-web/src/components/workspace/streaming-indicator.tsx
会话列表 UI frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
该智能体最近对话 frontend-web/src/components/workspace/agents/agent-recent-chats.tsx
左栏 frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx
产物预览 frontend-web/src/components/workspace/artifacts/
中文步骤文案 toolCalls.* frontend-web/src/core/i18n/locales/zh-CN.ts

20.2 前端:协议与状态

主题 路径
发消息 / useStream / cancel / 409 frontend-web/src/core/threads/hooks.ts
线程类型 frontend-web/src/core/threads/types.ts
标题与路由 frontend-web/src/core/threads/utils.ts
消息分组、思考、隐藏 frontend-web/src/core/messages/utils.ts
检索结果归一化 frontend-web/src/core/messages/search-results.ts
登录 API frontend-web/src/core/auth/api.ts
Bearer 存取 frontend-web/src/core/auth/index.ts
模型 API / 类型 frontend-web/src/core/models/api.ts、frontend-web/src/core/models/types.ts
LangGraph Client frontend-web/src/core/api/api-client.ts
Gateway fetch + 401 frontend-web/src/core/api/fetch-client.ts
Base URL frontend-web/src/core/config/index.ts、frontend-web/src/env.ts
写文件流式预览 frontend-web/src/core/artifacts/streaming-write-preview.ts
工具一句话 frontend-web/src/core/tools/utils.ts

20.3 后端:HTTP

主题 路径
登录 /me offline-backend-20260512/backend/app/gateway/routers/auth.py
JWT offline-backend-20260512/backend/app/gateway/auth/jwt.py
CORS offline-backend-20260512/backend/app/gateway/app.py
鉴权中间件 offline-backend-20260512/backend/app/gateway/auth_middleware.py
模型列表 offline-backend-20260512/backend/app/gateway/routers/models.py
会话 CRUD / search / state offline-backend-20260512/backend/app/gateway/routers/threads.py
runs/stream / cancel / join offline-backend-20260512/backend/app/gateway/routers/thread_runs.py
start_run、SSE、context 白名单 offline-backend-20260512/backend/app/gateway/services.py
上传 offline-backend-20260512/backend/app/gateway/routers/uploads.py
产物 offline-backend-20260512/backend/app/gateway/routers/artifacts.py
追问 offline-backend-20260512/backend/app/gateway/routers/suggestions.py
简化开放聊天(不要用来复刻本页) offline-backend-20260512/backend/app/gateway/routers/open_chat.py

20.4 后端:运行时与工具

主题 路径
跑图 + 发 SSE 事件 offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py
RunManager offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/manager.py
StreamBridge offline-backend-20260512/backend/packages/harness/deerflow/runtime/stream_bridge.py
消息序列化 offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py
Lead Agent offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py
协助卡中间件 offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py
协助工具 offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_tool.py
bash / write_file 等(description) offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py
工具总装 offline-backend-20260512/backend/packages/harness/deerflow/tools/tools.py
present_files offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/present_file_tool.py
标题 offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py
模型工厂 offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py

20.5 配置、种子、文档

主题 路径
roundtable-structure 种子 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/config.yaml
同上 SOUL offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md
模型 / 工具开关 offline-backend-20260512/backend/config.yaml
流式协议 offline-backend-20260512/backend/docs/STREAMING.md
登录契约 offline-backend-20260512/backend/docs/AUTH_LOGIN.md
上传 offline-backend-20260512/backend/docs/FILE_UPLOAD.md
iframe 嵌入备选 frontend-web/docs/embed-iframe-chat.md