1545 lines
62 KiB
Markdown
1545 lines
62 KiB
Markdown
# 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**。
|
||
下文用:
|
||
|
||
```text
|
||
GATEWAY = http://localhost:8001
|
||
LG = http://localhost:8001/api
|
||
```
|
||
|
||
所有对话接口都要带:
|
||
|
||
```http
|
||
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. 架构与端到端流程
|
||
|
||
```mermaid
|
||
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 源加进环境变量,例如:
|
||
|
||
```bash
|
||
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 用户名登录(推荐)
|
||
|
||
```http
|
||
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`
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"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`,旧票立刻失效。
|
||
|
||
后续所有请求:
|
||
|
||
```http
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
即 ``${token_type} ${access_token}``。
|
||
|
||
校验当前用户:
|
||
|
||
```http
|
||
GET {GATEWAY}/api/v1/auth/me
|
||
Authorization: Bearer <jwt>
|
||
```
|
||
|
||
```json
|
||
{ "id": "...", "email": "...", "system_role": "user", "needs_setup": false }
|
||
```
|
||
|
||
401 → 清本地 token,回到登录。
|
||
|
||
### 3.2 上游 Token 换票
|
||
|
||
适用于已有统一认证、URL 里带 `authToken` 的场景:
|
||
|
||
```http
|
||
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 存储建议
|
||
|
||
```ts
|
||
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 拉模型
|
||
|
||
```http
|
||
GET {GATEWAY}/api/models
|
||
Authorization: Bearer <jwt>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"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 新建会话
|
||
|
||
```http
|
||
POST {LG}/threads
|
||
Authorization: Bearer <jwt>
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"assistant_id": "roundtable-structure",
|
||
"metadata": { "agent_id": "roundtable-structure" }
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"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(必须,否则历史列表滤不出来)
|
||
|
||
```http
|
||
PATCH {LG}/threads/{thread_id}
|
||
{ "metadata": { "agent_id": "roundtable-structure" } }
|
||
```
|
||
|
||
为 merge,不会整表覆盖。
|
||
|
||
### 5.3 历史列表(按智能体过滤)
|
||
|
||
```http
|
||
POST {LG}/threads/search
|
||
{
|
||
"metadata": { "agent_id": "roundtable-structure" },
|
||
"limit": 50,
|
||
"offset": 0,
|
||
"query": null,
|
||
"status": null
|
||
}
|
||
```
|
||
|
||
返回 `ThreadResponse[]`。标题在 `values.title`(来自库里的 `display_name`)。
|
||
`query` 按标题模糊搜。系统会话(调度、会商工人线程)默认排除。
|
||
|
||
条数:
|
||
|
||
```http
|
||
POST {LG}/threads/count
|
||
(body 同上,limit/offset 忽略)
|
||
→ { "total": 12 }
|
||
```
|
||
|
||
列表项建议展示:
|
||
|
||
1. `values.title`;没有则用该会话第一条用户消息截断 50 字
|
||
2. `updated_at`
|
||
|
||
打开某条:跳到自己的路由并 `GET …/state` 拉消息。
|
||
|
||
### 5.4 打开已有会话(还原聊天记录)
|
||
|
||
优先:
|
||
|
||
```http
|
||
GET {LG}/threads/{thread_id}/state
|
||
```
|
||
|
||
`values` 里至少有:
|
||
|
||
```json
|
||
{
|
||
"title": "抽取方案结构",
|
||
"title_provisional": false,
|
||
"messages": [ /* 见第 8 节 */ ],
|
||
"artifacts": ["/mnt/user-data/outputs/flow.json"],
|
||
"todos": [{ "content": "…", "status": "in_progress" }]
|
||
}
|
||
```
|
||
|
||
也可:
|
||
|
||
```http
|
||
GET {LG}/threads/{thread_id}
|
||
```
|
||
|
||
带 metadata + 当前 `values`(含 messages)。
|
||
|
||
checkpoint 历史(一般不需要):
|
||
|
||
```http
|
||
POST {LG}/threads/{thread_id}/history
|
||
{ "limit": 10, "before": null }
|
||
```
|
||
|
||
只在最新一条 history 上带 `messages`,避免重复。
|
||
|
||
### 5.5 删除会话
|
||
|
||
两步都做才干净:
|
||
|
||
```http
|
||
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. 发起智能体对话(核心)
|
||
|
||
```http
|
||
POST {LG}/threads/{thread_id}/runs/stream
|
||
Authorization: Bearer <jwt>
|
||
Content-Type: application/json
|
||
Accept: text/event-stream
|
||
```
|
||
|
||
### 6.1 请求体(与本系统 AgentChatPage 对齐)
|
||
|
||
```json
|
||
{
|
||
"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**,文案类似「当前对话还在生成回答…」
|
||
|
||
响应头:
|
||
|
||
```http
|
||
Content-Type: text/event-stream
|
||
Content-Location: /api/threads/{thread_id}/runs/{run_id}
|
||
```
|
||
|
||
**立刻把 `run_id` 存下来**(停止、重连都靠它)。SSE 第一帧 `event: metadata` 里也会再给一次。
|
||
|
||
### 6.2 纯文本以外的用户消息
|
||
|
||
图片(模型 `supports_vision` 时):
|
||
|
||
```json
|
||
{
|
||
"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 **不够**,服务端可能还在跑。
|
||
|
||
```http
|
||
POST {LG}/threads/{thread_id}/runs/{run_id}/cancel?action=interrupt&wait=false
|
||
```
|
||
|
||
- 202:已受理;`wait=true` 等到停完返回 204
|
||
- `action=interrupt`:停下,保留当前 checkpoint(可继续聊)
|
||
- `action=rollback`:回滚到本轮开始前
|
||
|
||
列出该会话的 run:
|
||
|
||
```http
|
||
GET {LG}/threads/{thread_id}/runs
|
||
GET {LG}/threads/{thread_id}/runs/{run_id}
|
||
```
|
||
|
||
### 6.4 断线重连
|
||
|
||
SSE 帧带 `id:`。重连时:
|
||
|
||
```http
|
||
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` 常见:
|
||
|
||
```json
|
||
{ "type": "task_running", "task_id": "…", "message": "正在检索…" }
|
||
{ "type": "llm_retry", "message": "模型调用失败,正在重试…" }
|
||
{ "type": "context_compacting", "message": "正在压缩上下文…" }
|
||
```
|
||
|
||
后两个弹 toast 即可,不要当消息气泡。
|
||
|
||
### 7.3 `event: messages` 的 chunk(最重要)
|
||
|
||
`data` 是二元组:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"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 解析骨架
|
||
|
||
```ts
|
||
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 序列化后):
|
||
|
||
```json
|
||
{
|
||
"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)
|
||
|
||
渲染顺序示例(一轮典型技能执行):
|
||
|
||
```text
|
||
[用户] 请抽取结构
|
||
[步骤条]
|
||
思考
|
||
读取文件 SKILL.md
|
||
创建脚本
|
||
执行脚本
|
||
写入文件 /mnt/user-data/outputs/flow.json
|
||
展示文件
|
||
[助手] 已生成 flow-json,可在右侧预览。
|
||
```
|
||
|
||
若中途问用户:
|
||
|
||
```text
|
||
[步骤条] … 需要你的协助
|
||
[协助卡片] 需要你的确认
|
||
(等待用户下一轮 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 剥离算法(请直接用)
|
||
|
||
```ts
|
||
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`**:
|
||
|
||
```ts
|
||
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 / 技能)
|
||
|
||
常见:
|
||
|
||
```json
|
||
{
|
||
"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"`
|
||
|
||
结构化数据在:
|
||
|
||
```json
|
||
{
|
||
"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 提交内容格式(必须按这个拼,模型才认)
|
||
|
||
单张:
|
||
|
||
```text
|
||
我选择:任务→目的→行为体
|
||
我选择:A、B;补充:用户打的字
|
||
(只有自定义)用户打的字
|
||
```
|
||
|
||
多张:
|
||
|
||
```text
|
||
问题1「……」:我选择:A
|
||
问题2「……」:补充:……
|
||
```
|
||
|
||
然后:
|
||
|
||
```http
|
||
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 摘要 + 文件卡片。点击后:
|
||
|
||
```http
|
||
GET {GATEWAY}/api/threads/{thread_id}/artifacts/{path}
|
||
```
|
||
|
||
`path` 例:`mnt/user-data/outputs/flow.json`(虚拟前缀,不要用 Windows 盘符)。
|
||
`?download=true` 强制下载。HTML/SVG 会当附件,降低 XSS。
|
||
|
||
列表:
|
||
|
||
```http
|
||
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`:
|
||
|
||
```ts
|
||
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 追问建议(一轮结束后)
|
||
|
||
```http
|
||
POST {GATEWAY}/api/threads/{thread_id}/suggestions
|
||
{
|
||
"messages": [
|
||
{ "role": "user", "content": "…" },
|
||
{ "role": "assistant", "content": "…" }
|
||
],
|
||
"n": 3,
|
||
"model_name": "deepseek-chat"
|
||
}
|
||
```
|
||
|
||
```json
|
||
{ "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 空会话)。
|
||
|
||
```http
|
||
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。
|
||
|
||
```json
|
||
{
|
||
"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": []
|
||
}
|
||
```
|
||
|
||
发消息时:
|
||
|
||
```json
|
||
"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` |
|