diff --git a/frontend-web/docs/angular-智能体对话对接文档.md b/frontend-web/docs/angular-智能体对话对接文档.md new file mode 100644 index 00000000..34f0b92b --- /dev/null +++ b/frontend-web/docs/angular-智能体对话对接文档.md @@ -0,0 +1,1261 @@ +# 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 → 列模型/切模型 → 建会话/存聊天记录 → 流式对话 → 思考内容 → 步骤条(含创建脚本/执行脚本/写入文件及全部工具)→ 用户协助卡片 → 消息列表渲染 → 停止/重连/上传/产物**。 +**不包含**智能体配置(创建/编辑智能体、技能绑定等)。 + +--- + +## 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 +Content-Type: application/json +``` + +用 Bearer **不必**再带 CSRF。不要用 cookie 登录(`/login/local` 只把 JWT 写进 HttpOnly cookie,不适合 Angular 跨域 API 客户端)。 + +--- + +## 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"`(历史列表按此过滤) + +--- + +## 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`(勿用于生产)。 + +浏览器跨域读 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": "", + "token_type": "bearer", + "expires_in": 604800, + "user_id": "", + "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 +``` + +即 ``${token_type} ${access_token}``。 + +校验当前用户: + +```http +GET {GATEWAY}/api/v1/auth/me +Authorization: Bearer +``` + +```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= +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 || ''}`, + }); +} +``` + +--- + +## 4. 模型列表与切换 + +智能体配置不用做;**对话时选哪个大模型**要做。 + +### 4.1 拉模型 + +```http +GET {GATEWAY}/api/models +Authorization: Bearer +``` + +```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. 系统配置的第一个模型 + +每轮都可以换模型;换了只影响 **下一轮**,不会改写历史。 + +--- + +## 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 +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**。 + +--- + +## 6. 发起智能体对话(核心) + +```http +POST {LG}/threads/{thread_id}/runs/stream +Authorization: Bearer +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": "" + }, + "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`,忽略即可。 + +--- + +## 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 则拼接。同时要从正文里剥 `…`(见第 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 { + 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; +} +``` + +--- + +## 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` | **几乎不单独成气泡**,并进步骤条 / 协助卡片 | + +--- + +## 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`,去掉 `…` 这类内部标记 +- 附件:`additional_kwargs.files[]` → `{ filename, size, path, status }` +- 图片 block → `` +- 可选:编辑/删除(删除要改 checkpoint,对接初期可不做) + +### 9.2 助手最终回答 + +- Markdown(GFM 表格/代码块) +- 流式时按 token 追加;`values` 快照到达后用完整 `content` 替换 +- 正文里的 `` 必须剥掉(见第 10 节) +- 引用:技能检索结果会在步骤里展开,正文里可能有 `[n]` 引用角标 +- 复制 / 导出 Word 可选 + +### 9.3 流式指示 + +`busy` 且还没有最终 `assistant` 气泡时,在列表底部显示跳动的「正在生成」。 +上传文件未完成时可插一条 mock AI:`additional_kwargs.element === "task"`,文案「文件上传中,请稍候...」。 + +--- + +## 10. 思考内容如何对接 + +思考 **没有** 独立 SSE 事件名,混在 AI 消息里。按优先级取: + +1. `additional_kwargs.reasoning_content`(字符串,多数内网模型走这条) +2. content block `{ type: "thinking", thinking: "…" }` +3. 正文里的 `…`(含流式 **未闭合** 的 ``) + +### 10.1 剥离算法(请直接用) + +```ts +const THINK_RE = /\s*([\s\S]*?)\s*<\/think>/g; +const THINK_OPEN = ''; + +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('')) { + 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` 时模型不应再吐思考;若仍出现 ``,继续剥掉以免露标签。 + +--- + +## 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`),展示时做并集兜底。 + +--- + +## 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。 + +--- + +## 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` 同理。 + +--- + +## 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 | 回到新建 | + +--- + +## 15. 文件上传(用户侧) + +先有 `thread_id`(可先 create 空会话)。 + +```http +POST {GATEWAY}/api/threads/{thread_id}/uploads +Authorization: Bearer +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}`。 + +--- + +## 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)。 + +--- + +## 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` + `` 剥离 +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. 参考实现位置(本仓库) + +| 主题 | 文件 | +|------|------| +| 对话页 | `frontend-web/src/pages/AgentChatPage.tsx` | +| 发消息 / SSE | `frontend-web/src/core/threads/hooks.ts` | +| 消息分组 | `frontend-web/src/core/messages/utils.ts` | +| 步骤条 | `frontend-web/src/components/workspace/messages/message-group.tsx` | +| 协助卡片 | `frontend-web/src/components/workspace/messages/message-list.tsx` | +| 中文步骤文案 | `frontend-web/src/core/i18n/locales/zh-CN.ts` → `toolCalls` | +| 登录 | `frontend-web/src/core/auth/api.ts` | +| 模型 | `GET /api/models` ← `app/gateway/routers/models.py` | +| 会话 / 状态 | `app/gateway/routers/threads.py` | +| 流式 | `app/gateway/routers/thread_runs.py` | +| SSE 帧 | `app/gateway/services.py` `format_sse` | +| 协助卡 payload | `deerflow/agents/middlewares/clarification_middleware.py` | +| 沙箱工具 description | `deerflow/sandbox/tools.py`(bash / write_file / …) | + +智能体种子:`offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/`(`id: roundtable-structure`,`name: 结构化抽取`)。 diff --git a/offline-backend-20260512/backend/.model-config-backups/config.20260817T130658950404Z.yaml b/offline-backend-20260512/backend/.model-config-backups/config.20260817T130658950404Z.yaml new file mode 100644 index 00000000..6c45e6dc --- /dev/null +++ b/offline-backend-20260512/backend/.model-config-backups/config.20260817T130658950404Z.yaml @@ -0,0 +1,609 @@ +# Offline backend template for DeerFlow +config_version: 8 +log_level: info + +# Gateway FastAPI 监听端口。圆桌三路由(intent / recommend / multi_agent)的同进程 +# loopback 调用会拼 http://127.0.0.1: 打到本机其它 endpoint,所以必须与 +# 实际监听端口一致(scripts/start-all.sh、Makefile 默认 8001)。 +# 部署机改了端口请同步改这里;也可设环境变量 DEER_FLOW_GATEWAY_PORT 临时覆盖。 +gateway: + port: 8001 + +# Optional Redis-backed global concurrency coordination. +# Leave disabled to keep the existing in-process behavior. +# When enabled, Redis operations are short-timeout and fail-open by default: +# if Redis is slow or unavailable, user Q&A continues with local-only limits. +concurrency: + redis: + enabled: false + url: redis://127.0.0.1:6379/0 + password: null + # Prefix with the deployment/system id because Redis may be shared by + # multiple projects. Keep this unique per system to avoid cross-project + # leases affecting each other. + namespace: cmzs_deerflow:concurrency + socket_connect_timeout_ms: 80 + socket_timeout_ms: 80 + acquire_timeout_ms: 120 + operation_timeout_ms: 80 + circuit_breaker_failures: 3 + circuit_breaker_cooldown_seconds: 30 + lease_ttl_seconds: 90 + heartbeat_interval_seconds: 20 + fail_open: true + +models: +- name: deepseek-chat + display_name: DeepSeek Chat + use: langchain_openai:ChatOpenAI + model: deepseek-chat + api_key: $DEEPSEEK_CHAT_API_KEY + base_url: https://api.deepseek.com + request_timeout: 600.0 + max_retries: 2 + max_tokens: 4096 + temperature: 1.0 + supports_vision: false + supports_thinking: true +- name: zai-org/GLM-5-FP8 + display_name: zai-org/GLM-5-FP8 + use: langchain_openai:ChatOpenAI + model: zai-org/GLM-5-FP8 + api_key: $GLM5_FP8_API_KEY + base_url: https://api.us-west-2.modal.direct/v2 + request_timeout: 600.0 + max_retries: 2 + max_tokens: 4096 + temperature: 1.0 + supports_vision: true + supports_thinking: true + supports_reasoning_effort: false +- name: deepseek-ai/DeepSeek-V4-Flash + display_name: deepseek-ai/DeepSeek-V4-Flash + use: langchain_openai:ChatOpenAI + model: deepseek-ai/DeepSeek-V4-Flash + api_key: $SILICONFLOW_API_KEY + base_url: https://api.siliconflow.cn/v1/ + max_tokens: 4096 + temperature: 1.0 + supports_vision: true + supports_thinking: true + when_thinking_disabled: + extra_body: + enable_thinking: false +- name: inclusionAI/Ling-flash-2.0 + display_name: inclusionAI/Ling-flash-2.0 + use: langchain_openai:ChatOpenAI + model: inclusionAI/Ling-flash-2.0 + api_key: $SILICONFLOW_API_KEY + base_url: https://api.siliconflow.cn/v1/ + max_tokens: 4096 + temperature: 1.0 + supports_vision: true + supports_thinking: true +- name: deepseek-ai/DeepSeek-V4-Pro + display_name: deepseek-ai/DeepSeek-V4-Pro + use: langchain_openai:ChatOpenAI + model: deepseek-ai/DeepSeek-V4-Pro + api_key: $SILICONFLOW_API_KEY + base_url: https://api.siliconflow.cn/v1/ + max_tokens: 4096 + temperature: 1.0 + supports_vision: true + supports_thinking: true +- name: qwen3.6-flash + display_name: qwen3.6-flash + use: langchain_openai:ChatOpenAI + model: qwen3.6-flash + api_key: sk-sp-H.XDIPL.Zgvs.MEQCIBasaS2RPHwgNUpGVfFJeRaTp5UgmZ4Y2rCVGNtpwfD1AiBTFg3S-uE_gqVDnHgdyEhDwcfaVRLi92xviye80AZnkg + base_url: https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 + request_timeout: 30.0 + max_retries: 2 + stream_usage: true + supports_thinking: true + supports_reasoning_effort: false + supports_vision: false +- name: qwen3.6 + display_name: qwen3.6 + use: langchain_openai:ChatOpenAI + model: qwen3.6 + api_key: sk-6AqP_UYpPYJKdclYWR-FSA + base_url: http://119.254.86.251:8001/v1 + request_timeout: 30.0 + max_retries: 2 + stream_usage: true + supports_thinking: false + supports_reasoning_effort: false + supports_vision: false + # - name: deepseek-v4-flash + # display_name: DeepSeek V4 Flash + # use: langchain_openai:ChatOpenAI + # model: deepseek-v4-flash + # api_key: sk-3fee562f834a4c08bb0d471ef44cb445 + # base_url: https://api.deepseek.com + # request_timeout: 600.0 + # max_retries: 2 + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: false + # supports_thinking: true + # - name: deepseek-v4-pro + # display_name: DeepSeek V4 Pro + # use: langchain_openai:ChatOpenAI + # model: deepseek-v4-pro + # api_key: sk-e1c22b7931dc446f8b67e57878edd105 + # base_url: https://api.deepseek.com + # request_timeout: 600.0 + # max_retries: 2 + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: false + # supports_thinking: true + # - name: MiniMax-M2.7 + # display_name: MiniMax-M2.7 + # use: langchain_openai:ChatOpenAI + # model: MiniMax-M2.7 + # api_key: sk-cp-mrsAGtzDpUl0TpOBPVoiRG_pWF1kDDN9_mDAnONBs6HIPcNUexSKtSbx2QOxfKBvJT6Xw6hDTV6hdkJYFWfNA8UtHQ08TiGJiosdRoW6q1sGb6Cb7D85NHk + # base_url: https://api.minimaxi.com/v1 + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: true + # supports_thinking: true + # - name: glm-5.1 + # display_name: glm-5.1 + # use: langchain_openai:ChatOpenAI + # model: glm-5.1 + # api_key: 75121e4bed7640d5b71ac95cf8b4310a.RA3GyRBAR3kv7VrR + # base_url: https://open.bigmodel.cn/api/paas/v4/ + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: true + # supports_thinking: true + # - name: deepseek-ai/DeepSeek-OCR + # display_name: deepseek-ai/DeepSeek-OCR + # use: langchain_openai:ChatOpenAI + # model: deepseek-ai/DeepSeek-OCR + # api_key: sk-beshfxfxfrllsczpqfrgjbugfbpzxgqgedbehceytpcvvtyb + # base_url: https://api.siliconflow.cn/v1/ + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: true + # supports_thinking: true + # - name: zai-org/GLM-5-FP8 + # display_name: zai-org/GLM-5-FP8 + # use: langchain_openai:ChatOpenAI + # model: zai-org/GLM-5-FP8 + # api_key: modalresearch_gWu46FDpVLYZWPVEA3b_2fq8Jw2UJiKMgEm_Rxmn1SA + # base_url: https://api.us-west-2.modal.direct/v1 + # request_timeout: 600.0 + # max_retries: 2 + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: true + # supports_thinking: true + # - name: Qwen/Qwen3.6-27B + # display_name: Qwen/Qwen3.6-27B + # use: langchain_openai:ChatOpenAI + # model: Qwen/Qwen3.6-27B + # api_key: sk-ighylafootabtuthwueirmoycccyqtdgbifyhozduvolkdyt + # base_url: https://api.siliconflow.cn/v1/ + # max_tokens: 4096 + # temperature: 1.0 + # supports_vision: true + # supports_thinking: true + # SiliconFlow 混合推理模型关思考用「请求体顶层 enable_thinking: false」, + # 不认 chat_template_kwargs / thinking.type 那两种嵌套形状。声明这个后, + # 网页端「关闭思考」对本模型才生效(开放接口另由 force_disable_thinking 兜底)。 +# 识图(图片识别)配置。 +# - model_name:指定一个 models[] 中的模型名,作为“默认识图大模型”。view_image 工具会把 +# 图片交给该模型识别,并把识别出的文字结果返回给主对话模型 —— 这样即使主对话模型不支持 +# 视觉,也能“看图”。请改成你实际用于识图的模型名。 +# - 若留空 / 注释掉 model_name,则回退到主对话模型自身的视觉能力(需该模型 supports_vision: true)。 +# - prompt:不带具体问题时使用的默认识图指令(可选)。 +# - max_tokens:识图模型输出上限(可选,默认用该模型自身的配置)。 +vision: + model_name: deepseek-ai/DeepSeek-OCR + # prompt: 请详细描述这张图片的内容。 + # max_tokens: 2048 + +# 深度研究可选的报告配图。默认关闭;仅在部署方配置文生图模型后,前端才会展示开关。 +# 使用 OpenAI 兼容的 POST {base_url}/images/generations 协议,并强制要求 b64_json +# 返回,以便将图片保存为当前研究会话的受鉴权产物,而不是引用会过期的公网 URL。 +deep_research: + image_generation: + enabled: true + provider: openai_compatible + base_url: https://token-plan.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation # 例如 https://api.openai.com/v1 + api_key: sk-sp-H.XDIPL.Zgvs.MEQCIBasaS2RPHwgNUpGVfFJeRaTp5UgmZ4Y2rCVGNtpwfD1AiBTFg3S-uE_gqVDnHgdyEhDwcfaVRLi92xviye80AZnkg # 生产环境建议填 $DEEP_RESEARCH_IMAGE_API_KEY + model: wan2.7-image # 例如 gpt-image-1 + size: 1024x1024 + # quality: standard + timeout_seconds: 120 + +# browser_act 相关配置 +browser_context: + enabled: true + actions_enabled: true + +# 上传文件识别配置。 +# - auto_convert_documents: true 时,上传的 PDF / Word / Excel / PPT 会在服务端自动转换为 +# Markdown(.md),agent 通过 read_file / grep 读取其内容 —— 即“识别”文档内容。 +# 纯文本类(txt / csv / md / json / 代码等)无需转换,agent 用 read_file 直接读取。 +# 图片(png/jpg/webp)由 view_image + 上面的 vision 识图模型处理。 +# - pdf_converter: auto(优先 pymupdf4llm,缺失时回退 markitdown)| pymupdf4llm | markitdown +uploads: + auto_convert_documents: true + pdf_converter: auto + +title: + enabled: true + max_words: 6 + max_chars: 60 + prompt_template: | + 请为下面这段对话生成一个简洁的标题,概括对话的核心主题。 + + 要求: + - 标题必须使用简体中文 + - 尽量精炼,不超过 {max_words} 个词 + - 只返回标题本身,不要加引号、不要解释、结尾不要标点 + + User: {user_msg} + Assistant: {assistant_msg} + +# 上下文压缩(对话摘要):长对话接近上下文上限时,把较早的消息总结成一段摘要, +# 只保留最近若干条原始消息,避免撑爆模型上下文窗口。 +# - trigger:触发条件,可配多个,任意一个先满足就压缩(OR 关系)。真正决定何时 +# 压缩的是 trigger(不是 keep)。type=tokens 按 token 总数;type=messages 按条数。 +# - keep:压缩后保留多少最近的上下文(tokens / messages)。 +# - trim_tokens_to_summarize:喂给摘要模型的消息最多裁到多少 token。 +# 当前为「测试值(易触发)」:发到服务器后稍微聊几句就会触发,可在 Gateway 日志里 +# grep「上下文压缩」/「context compaction」确认生效;确认后再调回生产值 +# (trigger tokens 60000 / messages 80,keep tokens 16000)。 +summarization: + # 开关已移到聊天框下方(参考文献下面)的「上下文压缩」按钮,默认关闭。 + # 这里 enabled 仅作为非 Web 路径(渠道/定时任务)的后备开关;下面的 trigger/keep + # 等仅是调参,Web 端是否压缩由前端按钮(summarization_enabled)决定。 + enabled: false + # model_name: deepseek-chat # 留空则用主对话模型 + trigger: + - type: tokens + value: 120000 + - type: messages + value: 160 + keep: + type: tokens + value: 40000 + trim_tokens_to_summarize: 8000 + +tool_groups: + - name: web + - name: file:read + - name: file:write + - name: bash + +tools: + - name: web_search + group: web + # DuckDuckGo search — no API key required. To switch to the deployment's + # internal search service later, restore the configurable_search block + # preserved in the comments below (fill in the real values) and point + # `use` back to deerflow.community.configurable_search.tools:web_search_tool. + use: deerflow.community.ddg_search.tools:web_search_tool + enabled: true + max_results: 5 + # ── configurable_search alternative (deployment-internal service) ── + # endpoint: "https://xxx.xxx.xxx/searchName" + # verify_ssl: false + # timeout: 30 + # result_url_template: "https://xxx.xxx.xxx/mindId/xxx?recUuid={recUuid}" + # The runtime query always overwrites payload.query; all other fields are + # sent unchanged so deployment-specific request parameters stay in config. + # payload: + # query: "" + # xxx: "xxxx" + + - name: web_fetch + group: web + use: deerflow.community.jina_ai.tools:web_fetch_tool + timeout: 15 + + - name: ls + group: file:read + use: deerflow.sandbox.tools:ls_tool + + - name: read_file + group: file:read + use: deerflow.sandbox.tools:read_file_tool + + - name: glob + group: file:read + use: deerflow.sandbox.tools:glob_tool + max_results: 200 + + - name: grep + group: file:read + use: deerflow.sandbox.tools:grep_tool + max_results: 100 + + - name: write_file + group: file:write + use: deerflow.sandbox.tools:write_file_tool + + - name: str_replace + group: file:write + use: deerflow.sandbox.tools:str_replace_tool + + - name: bash + group: bash + use: deerflow.sandbox.tools:bash_tool + + # browser_act 相关配置 + - name: browser_fetch_page + group: web + use: deerflow.tools.builtins.browser_context_tool:browser_fetch_page_tool + + # browser_act 相关配置 + - name: browser_act + group: web + use: deerflow.tools.builtins.browser_act_tool:browser_act_tool + +sandbox: + use: deerflow.sandbox.local:LocalSandboxProvider + allow_host_bash: true + mounts: + - host_path: C:\Users\iie\Desktop\deerflow-new\offline-backend-20260512\backend\knowledge\knowledge_base + container_path: /mnt/knowledge + read_only: true + +skills: + path: skills + security_scan: + enabled: false + +skill_evolution: + enabled: true + curator: + enabled: true # 每 24 小时自动清理 + 合并 skill + interval_hours: 24 + review_worker: + enabled: true # 被动复盘开关 + min_tool_calls: 5 # 触发阈值:本轮至少 5 次工具调用,与 prompt 中"≥5 步"硬条件一致 + max_messages: 60 # 传给 LLM 的最大消息条数 + timeout_seconds: 120 # 单次 review 超时(秒) + model_name: # 留空则使用主模型 + +agents_api: + enabled: true + +# 登录鉴权扩展:在基础登录之上叠加两种自定义模式。 +auth_login: + # 用户名直登口令门:开关打开后,/login/<用户名> 直登必须再带 ?password=口令, + # 口令不对直接 401。password 留空则默认口令为 123ewq。仅作用于用户名直登。 + username_login: + require_password: false + password: '' + # token 换登录:前端用 ?authToken=上游token 进入,后端调用 token_info_url + # (把该 token 直接拼成 Authorization: Bearer 头)换出用户名,再登录; + # 用户名不在库里时自动注册。 + token_login: + enabled: true + token_info_url: 'https://ch1.b.uat.4.cn/consumer/login/getTokenInfo' + # 已弃用:鉴权头现由前端传入的 token 拼成 "Bearer ",此项不再生效,保留仅兼容。 + service_authorization: 'Bearer admin' + timeout_seconds: 10 + # 上游是 HTTPS。UAT/内网常用自签或私有 CA 证书,默认 verify=true 会握手失败导致 + # 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。 + verify_ssl: false + ca_cert_path: '' # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验 + # 离线测试映射:token -> 用户名。命中的 token 跳过 token_info_url 调用,直接当成 + # 该用户登录——外网调不通 getTokenInfo 时用来本地全链路联调。生产留空即可(不生效)。 + # 例:用 /login/任意?authToken=123ewq 进入,即以 admin 身份登录。 + mock_tokens: + '123ewq': 'admin' + +# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口, +# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。 + +database: + backend: sqlite + sqlite_dir: .deer-flow/data + pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略) + max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow + pool_timeout: 30 # 池满时等待空闲连接的秒数,超时报错 + pool_recycle: 1800 # 连接回收周期(秒);防火墙/pgbouncer/MySQL wait_timeout 静默断连兜底。多 worker 部署:每 worker 上限 = pool_size + max_overflow,总数 × worker 数 < DB max_connections + echo_sql: false + +checkpointer: + type: sqlite + connection_string: .deer-flow/data/deerflow.db + +# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。 +# 三层覆盖优先级(高->低): +# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处 +memory: + enabled: true + version: + v1 # 记忆版本: 'v1'(原版 DeerFlow, memory.json + LLM 自动提取) + # 'v2'(USER.md/MEMORY.md + 可选 Hindsight) + provider: + hindsight # 'builtin' | 'hindsight'(外部 provider 名) + # builtin 始终启用,这里只决定 external 选哪个 + # (version=v1 时此字段忽略) + + # V1 专属配置 —— version=v1 时生效 + v1: + max_facts: 100 # 最多保留的事实条数,超出时驱逐最低置信度的事实 + fact_confidence_threshold: 0.7 # 事实置信度阈值,低于此值不保存 + debounce_seconds: 30 # 对话结束后延迟多少秒才触发 LLM 提取 + model_name: '' # 用于事实提取的 LLM 模型名;空则继承全局默认模型 + token_budget: 2000 # 注入系统提示词的 token 预算 + builtin: + enabled: true + memory_char_limit: 2200 + user_char_limit: 1375 + deduplicate_on_load: true + + hindsight: # provider=hindsight 时生效,需另行部署 Hindsight 服务 + mode: local_external # 'cloud' | 'local_embedded' | 'local_external' + api_url: http://47.88.25.99:18888 + api_key: + '' # 部署 Hindsight 后填,或改成 $HINDSIGHT_API_KEY 并设同名环境变量 + # 注意:写成 $VAR 但环境变量未设置会导致配置加载失败 + bank_id_template: 'deerflow-user-{user_id}' + work_bank_id_template: 'deerflow-work-{user_id}-{agent_id}' + memory_mode: context # 'context' | 'tools' | 'hybrid' + bank_mission: '你是一个中文记忆助手,负责从对话中提炼和存储重要信息。所有记忆内容必须使用中文记录。' + bank_retain_mission: '请从以下对话中提炼重要信息,包括用户的偏好、需求、背景知识和关键事实。所有提炼的内容必须用中文表达。' + recall_budget: mid # 'low' | 'mid' | 'high' + recall_max_tokens: 4096 + recall_max_input_chars: 800 + auto_recall: true + auto_retain: true + retain_async: true + retain_every_n_turns: 1 + retain_user_prefix: 'User' + retain_assistant_prefix: 'Assistant' + + # background_extraction:预留功能,代码尚未实现 —— 仅设计文档存在。 + # 待对应版本支持后,取消下面整段注释即可启用(漏记兜底)。 + # background_extraction: + # enabled: false # 默认关闭,作为漏记兜底 + # debounce_seconds: 30 + # model_name: ~ # null = 用默认模型 + # fact_confidence_threshold: 0.7 + # max_facts_per_extraction: 10 + # correction_detection: true + # reinforcement_detection: true + + injection: + enabled: true + max_tokens: 2000 + include_builtin: true + include_hindsight: true + context_tag: 'memory-context' # 包裹 Hindsight recall 的标签名 + + security: + scan_content: true # 注入/外渗模式扫描 + block_invisible_unicode: true + # streaming_scrubber:预留字段,代码尚未实现 —— 待版本支持后取消注释。 + # streaming_scrubber: true # SSE 流前清洗 + +# AI 写作子系统 —— 素材收集专家专用配置。 +# 注意:这三项必须嵌套在 ai_writing: 段下;写到顶层会被 _ai_writing_config() +# (app/gateway/routers/ai_writing.py:_ai_writing_config) 直接忽略。 +ai_writing: + keyword_model: deepseek-chat # 检索词生成专用模型,留空回退写作主模型 + rank_model: deepseek-chat # 素材排序专用模型,留空回退写作主模型 + max_materials: 20 # 合并去重后的素材总量上限,范围 [1, 100],默认 20 + # 用「真实智能体运行时」跑配置技能(read SKILL.md + 全套工具 + 沙箱),让技能的真实 + # 检索逻辑(调内网接口/MCP/读文件)真正执行,而不是塞进通用 web/ES 检索 provider。 + # 内网技能必须开 true,否则技能会走通用 provider、连不上内网而报 ConnectError。 + # 仅对非「知识库检索(knowledge-base-search)」的配置技能生效;失败回退直连检索。 + researcher_skill_agent: true + # 最终兜底:当所有技能都没检索到素材时,直连内网知识库 ES 接口兜底检索(避免素材全空)。 + # 配上你内网 ES 接口地址即启用;留空则不启用该兜底。 + # 接口契约:POST {url} 体 {"query","knowledge_base_name","ts":"yes"} → [{"page_content","metadata":{m_title,content,recUuid,m_publish,source1}}] + intranet_search_url: "" # 例如 http://10.106.57.180:7863/knowledge_base/search_docs_es + intranet_knowledge_base: "samples" # 兜底检索用的知识库名 + intranet_search_timeout: 300 # 内网检索超时时间(秒),代码层强制最低 300;需要更长可调大 + # 内网接口诊断开关:开启后即使技能已检索成功,也照样把内网接口跑完并把执行结果 + # (端点/每个检索词命中数/报错/原始响应片段)记到 material_package.intranet_debug 里, + # 便于在接口返回(devtools)排查内网接口到底通不通。生产建议关掉(会多跑一次内网)。 + intranet_search_debug: false + # 四阶段「内置功能型智能体」开关(按阶段灰度)。true = 该阶段节点改为加载 + # .deer-flow/agents/ai-writing-/ 的 SOUL.md + config(admin 可在智能体 + # 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。 + # 改完保存即生效(config 按 mtime 热加载),新写作会话立即按新开关运行。 + use_builtin_agents: + researcher: false # 素材收集专家(检索词生成 / 素材排序两个子任务) + outliner: false # 大纲规划师(大纲生成 / 按反馈调整) + writer: false # 作家(逐章正文写作,逐章流式与严格模式求助逻辑不变) + editor: false # 编辑(事实/逻辑/语言三维度审查) + +# Optional independently deployed WeKnora integration. DeerFlow owns only +# these connection values; database/Redis/storage/parser/model settings stay +# in WeKnora's own docker-compose/.env/config files. A non-empty api_base_url +# enables WeKnora mode; leaving it empty preserves the existing LLMWiki iframe. +# admin_email/admin_password are used only by DeerFlow's server-side WeKnora +# detail-page iframe proxy. Prefer environment variables in production, e.g. +# admin_email: "$WEKNORA_ADMIN_EMAIL" +# admin_password: "$WEKNORA_ADMIN_PASSWORD" +llmwiki: + weknora: + api_base_url: "http://119.254.86.251:8088/deerflow-api" + web_base_url: "http://119.254.86.251:8088" + admin_email: "lqq@qq.com" + admin_password: "1qaz@WSX" + +log_system: + enabled: true + url: 'https://your-log-system.example.com' + label: '日志系统' + +task_deeplink: + # 线上接口未通时用假数据;三个开关互相独立。 + test: true # 目的树查询走 mock.purpose_detail 假数据 + delete_test: true # 删除接口走 mock.delete 假数据(该接口尚未开发完) + action_test: true # 行动→任务解析走 mock.action_detail 假数据(xdfx 行动id→任务id) + # 开放接口 POST /api/open/3qfx/ask 按 taskId 拉取任务详情拼开场白;上游不可达时打开下面开关走 mock.cop_task_detail。 + cop_task_detail_test: true + cop_task_detail_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/cop-task-detail" + purpose_detail_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail" + # rwfx 与 xdfx 共用同一删除接口:rwfx 追加 ?taskId=(不传 actionId);xdfx 追加 ?actionId=(不传 taskId,直接不出现)。 + delete_url: "https://ch1.b.uat.4.cn/bwjt/dropSixOrSevenData" + treemap_edit_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit" + treemap_next_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next" + # xdfx 深链传入的是「行动id」,先调该接口(追加 /{actionId})拿 data.taskId 解析出真正「任务id」。 + action_detail_url: "https://ch1.b.uat.4.cn/consumer/plan/action" + # xdfx 删除前「行动历史是否存在」预检接口(追加 ?actionId=);地址待提供,留空则前端视作无历史、不阻断删除。 + action_history_url: "" + # rwfx 目的树面板「3q详情」链接的目标页(地址待定,留空则前端提示未配置);跳转追加 ?id=任务id。 + detail_3q_url: "" + mock: + # test=true 时原样返回给前端(结构同真实 getTaskPurposeDetail 返回)。 + purpose_detail: + state: "200" + msg: "操作成功!" + data: + - purposeDto: { id: 5837, zzPurpose: "目的01", selectedFlag: 1, taskId: 3324, recDetailId: 4271 } + childrenList: + - actorDto: { id: 9101, taskId: 3324, purposeId: 5837, actorName: "行为体01", actorOrgan: "行为体01", recReason: "行为体01理由", selectedFlag: 1 } + childrenList: + - expectedBehaviorDto: { id: 6910, taskId: 3324, purposeId: 5837, behavior: "预期01", recReason: "预期01理由", selectedFlag: "1" } + childrenList: + - drivingFactorDto: { id: 7964, taskId: 3324, purposeId: 5837, drivingFactor: "预期01因素", selectedFlag: 1 } + childrenList: + - narrationDto: { id: 6500, purposeId: 5837, taskId: 3324, narration: "叙事01", recReason: "叙事01理由", selectedFlag: 1 } + childrenList: [] + - actorDto: { id: 9102, taskId: 3324, purposeId: 5837, actorName: "行为体02", actorOrgan: "行为体02", recReason: "行为体02理由", selectedFlag: 1 } + childrenList: + - expectedBehaviorDto: { id: 6911, taskId: 3324, purposeId: 5837, behavior: "预期02", recReason: "预期02理由", selectedFlag: "1" } + childrenList: + - drivingFactorDto: { id: 7965, taskId: 3324, purposeId: 5837, drivingFactor: "预期02因素", selectedFlag: 1 } + childrenList: + - narrationDto: { id: 6501, purposeId: 5837, taskId: 3324, narration: "叙事02", recReason: "叙事02理由", selectedFlag: 1 } + childrenList: [] + # delete_test=true 时原样返回给前端。改 success 测「成功覆盖 / 不可覆盖」两条分支。 + delete: + message: "行动已经开始策划,不可覆盖" + success: true + # action_test=true 时原样返回给前端(结构同真实 /consumer/plan/action/{actionId} 返回)。 + # 前端取 data.taskId 作为真正的「任务id」(xdfx 深链 ?taskId= 传入的是 data.id 这个行动id)。 + action_detail: + state: "200" + msg: "操作成功!" + data: + id: 27354 + purposeId: 2850 + taskId: 2055 + actionName: "" + actionStatus: 25 + narration: "" + # cop_task_detail_test=true 时原样返回给开放接口 /api/open/3qfx/ask(结构同真实 cop-task-detail 返回)。 + cop_task_detail: + data: + id: 2055 + overview: "暑期文旅消费高峰期间的舆情态势综合研判与引导任务" + taskDirection: "正向引导 + 风险预警" + taskName: "任务01:暑期文旅舆情综合研判" + taskContent: "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。"