62 KiB
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 图。
真正选中哪个智能体靠三层同时带上(缺一不可,建议都传):
- 请求体
assistant_id = "roundtable-structure"(文件系统/库表里的 id,不是中文名) context.agent_id = "roundtable-structure"- 会话
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,否则可能关不干净。
模型解析顺序(后端):
- 本轮
context.model_name - 智能体自己的
config.yaml → model - 系统配置的第一个模型
每轮都可以换模型;换了只影响 下一轮,不会改写历史。
参考代码
| 说明 | 路径 |
|---|---|
| 前端拉模型 | 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 }
列表项建议展示:
values.title;没有则用该会话第一条用户消息截断 50 字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再以 SSEevent: 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 分桶):
- 正文:把每次
content(字符串)往后拼;如果是 block 数组,只拼type==="text"的text - 思考:
additional_kwargs.reasoning_content一般是 累计全文(以最新一帧为准);若是 delta 则拼接。同时要从正文里剥<think>…</think>(见第 10 节) - 工具调用:按
tool_call_chunks[].index拼name/args(args 是 JSON 字符串碎片,拼完再JSON.parse) - 工具结果:会出现
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(已过滤隐藏消息):
human→ 一组humanai且tool_calls含present_files→assistant:present-files(正文 + 文件卡片)ai且tool_calls含task→assistant:subagent(子任务卡)ai有思考或有其它 tool_calls → 并入(或新建)assistant:processing(步骤条)- 同一条
ai既有思考又有最终正文、且没有 tool_calls → processing 一组 再 出一组assistant气泡 tool且name === ask_clarification→ 先塞进当前 processing,再单独开assistant:clarification- 其它
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 消息里。按优先级取:
additional_kwargs.reasoning_content(字符串,多数内网模型走这条)- content block
{ type: "thinking", thinking: "…" } - 正文里的
<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:
- 若有思考 → push
{ type: "reasoning", reasoning } - 遍历
tool_calls,跳过name === "task" - 用
tool_call.id在组内找对应type==="tool"的消息,作为result 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. 最小对接顺序(验收清单)
POST /api/v1/auth/login/username→ 存 JWT- CORS 已包含 Angular Origin
GET /api/models→ 下拉,选中项的name写入context.model_namePOST /api/threads+metadata.agent_id=roundtable-structurePOST /api/threads/{id}/runs/stream,assistant_id与context.agent_id都等于roundtable-structure- 解析 SSE:
metadata→messages增量 →values覆盖 →end - 消息分组:用户气泡 / 步骤条 / 协助卡片 / 最终 Markdown
- 步骤条至少覆盖:
bash(description=创建脚本/执行脚本)、write_file(写入文件)、read_file、ls、web_search、未知工具 - 思考:
reasoning_content+<think>剥离 - 协助卡片:选项 → 拼「我选择:」→ 新一轮 human
POST /threads/search还原列表;GET /state还原消息(含思考与步骤)- 停止:
POST …/runs/{run_id}/cancel - 409 toast
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 工程注意
- POST SSE 用 fetch,不要
HttpClient.get+ EventSource run_id从Content-Location或event: metadata取;取消必须打 cancelassistant_id用roundtable-structure,不要传「结构化抽取」- 历史过滤靠
metadata.agent_id,第一轮后务必 PATCH - 同一 thread 未
end不要再发,否则 409 - JWT 约 7 天,无 refresh
- 把
localhost:4200加入GATEWAY_CORS_ORIGINS - 可用 npm 包
@langchain/langgraph-sdk的Client(apiUrl: LG, defaultHeaders: { Authorization })少写 SSE 细节;UI 仍要自己做分组/步骤条/卡片 - 不要把思考、tool 结果、协助卡当成三条普通聊天记录平铺,观感会和现网差很多
- 步骤条 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 |