43 KiB
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Project Overview
This is a customized offline deployment of DeerFlow — a LangGraph-based AI super agent platform. The repo contains two sub-projects:
offline-backend-20260512/backend/— Python backend (FastAPI Gateway + LangGraph agent runtime)frontend-web/— Vite + React 19 frontend (TypeScript, pnpm)
Managed together via shell scripts in scripts/.
Offline Release Repository
Linux AMD64 离线发版脚本、wheelhouse、deploy.env 模板和部署文档统一维护在
knowledge-base-bj/deerflow-offline-deployment,不要把离线镜像或发版资产提交到
本业务源码仓库。正式发版从该仓库执行 scripts/build-latest-master.sh,脚本会读取
本仓库最新 master 的干净源码快照。
Commands
Full Application (from repo root)
./scripts/start-all.sh # Start backend + frontend as background daemons
./scripts/stop-all.sh # Stop both
./scripts/restart-all.sh # Restart both
./scripts/status.sh # Check running status
Logs and PID files are written to .runtime/logs/ and .runtime/pids/.
Backend Only (from offline-backend-20260512/backend/)
make install # Install Python dependencies (uv)
make dev # Run Gateway API with hot-reload on port 8001
make gateway # Run Gateway API without reload
make test # Run all tests: PYTHONPATH=. uv run pytest tests/ -v
make lint # Lint with ruff
make format # Format with ruff
Run a single test file:
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
Frontend Only (from frontend-web/)
pnpm install # Install dependencies
pnpm dev # Start Vite dev server on port 5174
pnpm build # Production build
pnpm typecheck # TypeScript type checking (tsc --noEmit)
Service Ports
| Service | Port | Notes |
|---|---|---|
| Backend (Gateway API) | 8001 | FastAPI + LangGraph runtime |
| Frontend (Vite) | 5174 | React SPA |
Frontend connects to backend via env vars VITE_BACKEND_BASE_URL and VITE_LANGGRAPH_BASE_URL (injected by scripts/start-frontend.sh).
Architecture
Backend
The backend (offline-backend-20260512/backend/) is split into two layers:
packages/harness/deerflow/— publishable agent framework (deerflow.*imports): LangGraph lead agent, middleware chain, sandbox, tools, MCP, memory, models, skills. Never imports fromapp.*.app/— application layer (app.*imports): FastAPI Gateway routers, IM channel integrations (Feishu, Slack, Telegram, DingTalk).
The Lead Agent (deerflow/agents/lead_agent/) is the single LangGraph graph entry point. It wraps a middleware chain (~18 middlewares assembled in strict order) around LLM calls, handling: thread isolation, sandbox lifecycle, uploads, memory, plan mode, vision, subagent delegation, and loop detection.
Middleware order matters — middlewares are appended in a fixed sequence; ClarificationMiddleware must always be last. See offline-backend-20260512/backend/AGENTS.md for the full ordered list.
Harness → App import firewall is enforced by tests/test_harness_boundary.py (runs in CI).
Workflow Studio (deerflow/workflows/ + /api/workflows*) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only inside an agent/skill node. Master switch and all limits live in config.yaml → workflows. See the backend AGENTS.md/CLAUDE.md「Workflow Studio」and docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md.
Frontend
The frontend (frontend-web/) is a React 19 + Vite SPA:
-
Routing: React Router v7; top-level routes in
src/pages/PageRoutes.tsx, workspace sub-routes insrc/pages/WorkspaceRoutes.tsx -
Pages:
ChatPage(thread chat),AgentChatPage(custom agent chat),AgentsPage/NewAgentPage(agent management),ScheduledTasksPage/ScheduledTaskRunDetailPage(scheduled tasks),LightAppManagementPage/LightAppListPage/LightAppIframeView(轻应用中心 — register/list/embed mini-apps) -
AI 写作·对话版(对话驱动写作台): a parallel new entry beside the classic AI-writing page (classic page/routes untouched) — route
/page/canvas/ai-writing-chat, sidebar 「问答管理 → 课题研究(对话版)」. Pageopen-canvas/pages/AIWritingChatPage.tsxreuses the engine (AIWritingContext+useStream+AIWritingDraftPanel); idle keeps the classic setup layout (WritingSetupChat+ collapsible form) and switches to the chat workbench once writing starts; left panelcomponents/ai-writing-chat/WritingChatPanel.tsx= unified chronological feed (ChatFlowTimeline.tsx, chat bubbles anchor-interleaved into the progress timeline so triggered events render below the user's message) + button-only intervention cards (ChatInterventionCards.tsx, inline textareas removed) + bottom unified smart composer. Intent resolution: frontend fast path +POST /api/ai-writing/sessions/{id}/intent→deerflow/agents/ai_writing/intent_router.py(per-pause action whitelist, high-cost confidence gates, payload cleaning); supports QA bypass, clarify follow-ups, control intents, and material-stage auto re-search + pending-intent resume. Teststests/test_ai_writing_intent.py; plan docfrontend-web/docs/ai-writing-对话驱动写作台-实现方案.md. -
轻应用中心 (Light App Center): a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers
window_openoriframemini-apps with a route-param builder: login params resolved fromlocalStorage.userInfo, theme params literal) and 应用列表 (card grid ofwindow_openapps,window.openlaunch).iframeapps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and dynamically injected into the sidebar bycore/page-layout/use-sidebar-menu.ts(useSidebarMenuItems), opening in theLightAppIframeViewhost. Client + React Query hooks instrategy-components/api/light-apps.ts; URL builder/constants instrategy-components/lib/light-app.ts. Backend:/api/light-apps(global/shared, admin-gated writes). -
菜单管理 (Menu Management, admin defaults + personal layouts):
MenuManagementPage(pages/MenuManagementPage.tsx) is available from「设置和更多」and the global Header account dropdown. Administrators edit the shared default menu; ordinary users edit only their own full/compact menu snapshot. For users with a saved snapshot, known node ids keep the personallabel/disabled/parentId/sortOrder(or compact placement/order), while administrator defaults for node ids absent from that snapshot flow through—so newly-added code menus and iframe light apps follow the current default without overwriting older personal choices. Temporarily absent dynamic nodes are preserved when an unrelated personal edit is saved;「恢复默认」deletes only that user's snapshot. The static tree (core/page-layout/sidebar-menu.ts) remains the source of icons/routes and administrator defaults remain sparse per-node deltas. Merge/snapshot helpers live incore/page-layout/menu-overrides.tsandcompact-menu-overrides.ts; full-menu writes carry the complete effective parent hierarchy for cycle/strict three-level validation while persisting only the sparse admin delta or full personal snapshot. Backend/api/menu-overridesusesmenu_overrides(default) +user_menu_override_configs(per-user snapshot);/api/compact-menu-overridesusescompact_menu_overrides(typeA/Bdefaults) +user_compact_menu_override_configs(per-user/type snapshots). Each website selects its compact type viaruntime-config.jsVITE_COMPACT_MENU_TYPE;CompactToolboxrenderstoolboxitems. -
舆情分析智能体(前端虚拟智能体): 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页
pages/SentimentAgentChatPage.tsx(路由agents/sentiment-analysis/chats/:thread_id)复用普通问答布局 +MessageList,输入框只有发送按钮;问答经 GatewayPOST /api/sentiment-agent/stream转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在runtime-config.js配置(VITE_SENTIMENT_AGENT_API_URL/_AUTH_USERNAME/_AUTH_PASSWORD)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块src/core/sentiment-agent/。 -
简洁模式用户引导 (Compact-mode onboarding tour): 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React
Guide组件驱动的分步引导。组件components/workspace/compact-mode-guide.tsx(挂在ChatPage):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的data-compact-guide标记定位(workspace-nav-chat-list/recent-chat-list/workspace-nav-menu/appearance-controls/custom-prompt-menu.tsx的CustomPromptBar(提示词栏,仅新对话落地态渲染)与其ManagePromptsDialog(prompt-dialog锚点)/ ChatPage 输入框容器),启动时按目标是否存在过滤步骤(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。提示词管理弹窗步骤是动态目标:弹窗平时不渲染,引导切到该步前先广播PROMPT_DIALOG_GUIDE_EVENT(custom-prompt-menu.tsx导出)让CustomPromptBar打开ManagePromptsDialog并接管(owner: true期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以owner: false收回弹窗,4 秒打不开则跳过该步防卡死。TDesign Guide × Radix 模态互操作:Radix 模态弹窗打开时会把body置pointer-events:none(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),styles/tdesign-theme.css末尾对.t-guide__overlay/__highlight/__popup/__wrapper显式pointer-events:auto兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStoragedeerflow.compact-guide-finished记忆(浏览器级);中途刷新/离开不记忆,下次进入还会再演示。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播compact-guide:start窗口事件直接启动,否则写 sessionStorage 请求并导航/chats/new由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。eligible排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。 -
智能体使用引导 (Agent onboarding tour, 三段式): 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件
components/workspace/agents/agent-guide.tsx(同一 TDesignGuide封装模式,与简洁模式引导共享记忆语义:仅显式点「完成/跳过」才写 localStoragedeerflow.agent-guide.<stage>.finished,中途刷新/离开不记忆)。自动演示仅 create / chat 两段(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的AgentGuide传autoStart={false},只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:①list(agent-gallery.tsx,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;完成/跳过就地结束(不跳转创建页,与 create 段无联动),最后一步仅提示进入创建页后会有配置引导;②create(NewAgentPage.tsx,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③chat(AgentChatPage.tsx,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一chat-input标记)、右侧详情面板;eligible限定普通智能体对话页(showAgentList && !isEmbedded && !taskCtxOn,即排除通用问答原地嵌入与任务深链工作区),三段共用isAgentGuideEnvironment()排除 embed/hideChrome/无界环境。高亮锚点data-agent-guide:列表页tabs/search/agent-card(卡片+列表两种布局的根元素)/new-agent-btn,创建页create-chat-input/create-basic/create-model/create-knowledge/create-soul/create-skills/create-save-btn,对话页chat-sidebar/chat-questions/chat-input/chat-detail;同样启动时按目标存在性过滤步骤(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播agent-guide:start:<stage>窗口事件强制重放(忽略已看过记忆)。 -
敏感词管理 (Sensitive Word Management): configures the 展示层脱敏映射 (原始词 → 替换词). Gated on the username
lqq(not the admin role) —getAccountDisplayName().toLowerCase() === "lqq"(username = email@prefix, elseuser_id, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check.src/lib/desensitize.tsships a frozen seed mapSEED_DESENSITIZE_MAP(the deployment seed) plus a live mutableDESENSITIZE_MAP(initialized from the seed sodesensitize()works before any fetch).DesensitizeWordsLoader(mounted inApp.tsxAppProviders) fetches/api/sensitive-wordson startup and, if non-empty, callsreplaceDesensitizeMap(...)to make the backend config the source of truth (empty/failed → keep seed). PageSensitiveWordsPage.tsx(route/page/strategy/admin/sensitive-words, opened from the 「敏感词管理」 item in the bottom-left settings dropdown ofcomponents/page-sidebar.tsx, present in both full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the whole set atomically (replace-all); a 「导入种子词」 button merges any missingSEED_DESENSITIZE_MAPentries into the editor. Client + hooks instrategy-components/api/sensitive-words.ts(wordsToMapfolds enabled rows into the runtime map). Backend:/api/sensitive-words(global/shared; open read, write allowed for admin or thelqqaccount), mirroring 菜单管理's store shape. 侧边栏 + 导航栏 menu labels are desensitized at the data source:useSidebarMenuItems/useActiveThirdLevel(core/page-layout/use-sidebar-menu.ts) map every node'slabelthrough a desensitizer built directly from the/api/sensitive-wordsquery (makeDesensitizer(wordsToMap(words)), falling back to the global seeddesensitizewhile empty) — this sidesteps thereplaceDesensitizeMapeffect-timing race. Onlylabelis rewritten;path/id/iconare untouched so routing/highlight are unaffected. All nav surfaces (page-sidebar.tsx,page-sidebar-dark.tsx,page-sidebar-v1.tsx,strategy-components/Header.tsx,PageRoutes.tsx) consume this hook and are covered;MenuManagementPageuses the raw tree so admins still edit real labels. -
技能上下线 (Skill Availability, owner/admin): the global
Skill.enabledflag is the online/offline switch. Taking a skill offline persistsskills.<name>.enabled=falseinextensions_config.jsonbut does not delete its directory or remove it from existing agent/workflow configurations; the row remains visible and bringing it online restores those references. Administrators may toggle every skill from 「技能管理」; ordinary users may toggle only their own non-built-incustomskills from 「技能 → 技能列表 → 我的」. Offline skills must be absent from every model-facing skill catalogue (leadprompt,search_skills,skill_list,skill_view, subagents, AI writing/deep research/roundtable and Workflow run-start validation), from agent skill pickers, and from direct sandbox access:read_file/ls/glob/grep/bashreject offline, unknown, root-wide and category-wide/mnt/skillspaths. The backend mutation isPUT /api/skills/{name}, authorizes either an administrator or the persisted owner of the target custom skill, patches the raw document so MCP placeholders and unrelated extension fields survive, uses atomic replacement when supported plus a Docker single-file-bind fallback, and refreshes the skill prompt cache. Launchers keep the runtime extensions file in a writable persisted directory. Existing agent/strategy-agent references stay visible as 「已下线」 and may be removed but not newly selected. -
技能地址管理 (Skill Address Management, admin-only): 扫描运行中的技能(
skills/{public,custom}下所有.md/.py)里的 http(s) URL 与 IP/IP:端口,按唯一地址聚合全部出现位置(跨 .md/.py),支持批量替换(如把 UAT 地址一次性改成生产)。主体抽成可复用组件SkillAddressesPanel(pages/SkillAddressesPage.tsx导出,默认导出SkillAddressesPage仍保留旧路由/page/strategy/admin/skill-addresses作直链回退),作为「技能地址管理」标签页嵌入管理员「技能管理」页pages/AdminSkillsPage.tsx(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」workspace-nav-menu.tsx→/page/workspace/admin/skills,不再单列于components/page-sidebar.tsx左下角设置下拉):打开自动扫描并带进度条(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有uat.4.cn→prod.4.cn)、每地址「替换为」输入框(可展开看file:line片段)、内联预览、应用、多关键词检索(空格分隔全部命中),以及修改记录面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可针对单次记录回退(二次确认)。client + NDJSON 流式解析在strategy-components/api/skill-addresses.ts。后端/api/skill-addresses(仅管理员):所有文件 IO 走asyncio.to_thread不阻塞事件循环;GET /scan(+/scan/stream)、POST /preview、POST /apply(+/apply/stream)、GET /backups、POST /rollback/{id};子串安全替换(基于 token span,不会误伤子串)、expected_total乐观锁(409)、写前自动磁盘备份到{skills_root}/.address-edits/(同时写一份{id}.manifest.json记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎deerflow/skills/address_scan.py,routerapp/gateway/routers/skill_addresses.py,teststests/test_skill_addresses.py。范围仅运行中技能,不动deploy/minimal/skills/。 -
按钮管理 (Task Button Management, admin-only): configures the 可配置跳转按钮 rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see
TaskFxWorkspace). Each button ={business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder};linkTypeisurl(外链window.open, optional append of?{idParam}={id}—idParamis the editable query-param name, e.g.id/task_id),business(内部切换到另一业务, reusesresolveTaskDeeplinkTarget, always task id), orpurpose(弹出当前任务的「任务目的」表格 via the sharedTaskPurposeDialog, no target — configurable for any page incl. xdfx).host(宿主路由) asks the embedding parent to navigate:runTaskButtonsendswindow.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)—targetis the host route,appendTaskId/idParambecome numericqueryParams(task id, or action id whenidKind:"action"— same selector as url buttons, xdfx only), origin fromruntime-config.jsVITE_HOST_NAVIGATE_ORIGIN(else ancestorOrigins/referrer; never*).idKind(task/action) only matters for xdfx: its deep-link?taskId=is an 行动id and the 任务id is resolved at login (fetchTaskIdFromAction) —TaskContextnow also stashes the rawactionId, so a url or host button can append either (defaulttask). PageTaskButtonsPage.tsx(route/page/strategy/admin/task-buttons, admin-gated, entry「按钮管理」in the bottom-left settings dropdown ofcomponents/page-sidebar.tsx, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the whole set atomically (replace-all), plus 「导入默认」 to merge code-seed defaults. Seed/fallback (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back todefaultTaskButtons(cfg)— rwfx's url buttons (编辑/下一步/3q详情) built from the live/api/public/config/task-deeplinkplus cross-business switch buttons for all three businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out:TaskFxWorkspaceno longer hardcodes 切换到三情分析,TaskActionsPanelkeeps only the non-jump 清除记录, andTaskPurposePaneldropped its 3q详情 button (now configurable) and defaults the 目的树 table to collapsed (chevron toggle). Helperscore/auth/task-buttons.ts(TaskButton,defaultTaskButtons,resolveTaskButtons,buttonsForBusiness,runTaskButton), client+hooksstrategy-components/api/task-buttons.ts. Backend:/api/task-buttons(global/shared; open read, admin replace-all), storedeerflow.persistence.task_buttons(tabletask_buttons, auto-created bycreate_all), wiredapp.state.task_button_store. Tests:tests/test_task_buttons.py. -
单智能体任务深链聊天记录(2026-10-19):
3qfx/rwfx/xdfx的会话历史使用独立task_deeplink_threads索引表及GET /api/task-deeplink-threads?task_id=&go_path=,前端由useTaskDeeplinkThreads读取,不再筛选通用聊天列表。LangGraph thread/checkpoint 仍是消息权威数据;专用表仅按(task_id, go_path)定位 thread。3qfx/rwfx按任务 id,xdfx按 URL 原始行动 id;新 thread 写metadata.{taskRecordId,taskGoPath,thread_type:"task_deeplink"}并自动登记,通用最近对话默认隐藏。迁移20261019_01只建表、不迁移/回填旧数据。 -
State: TanStack Query for server state, context providers (
SubtasksProvider,ArtifactsProvider,PromptInputProvider) scoped toWorkspaceRoutes -
LLM streaming:
@langchain/langgraph-sdkstreams messages from the backend via SSE -
UI: Radix UI primitives + TailwindCSS v4 + shadcn-style components in
src/components/ui/ -
Shims:
src/shims/providesnext/linkandnext/navigationpolyfills (original code was Next.js) -
Login URL params (
LoginPage.tsx, frontend-only — the backend never sees these): besides the auth params (ticket/userId/env/theme/yUserId/token/access_token/authToken/password), the page supports a task deep-link via?taskId=&goPath=. After login it calls the consumer APIGET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=(headerAuthorization: Bearer admin, auto-injected byutils/request.ts), stashes the fulldata(id/overview/taskDirection/taskName/taskContent) intosessionStorage["login.copTaskDetail"], then routes bygoPath: base codes3qfx/rwfx/xdfx→ that business's configured single agent Q&A inside a 任务工作区 (resolved via/api/business-mapping;resolveTaskDeeplinkTargetcallspersistTaskContext({taskId, agentId, openingText})and navigates to the…/chatsindex, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOTpendingAgentMessage); the-Asuffix (3qfx-A/…) → multi-agent roundtable withroundtablePrefill.businessCodeso the page auto-selects the configured 业务链条. xdfx's deep-link?taskId=is actually an 行动id, not a 任务id —resolveDeeplinkTaskId(goPath, rawId)(called inLoginPagebefore everything) detectsxdfx/xdfx-Aand resolves it viaGET {action_detail_url}/{actionId}→data.taskId(fetchTaskIdFromAction;action_test+mock.action_detailfor offline testing); the resolved 任务id then drivescop-task-detail, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id:resolveTaskDeeplinkTargetsetspartitionByAction:true+ keeps the raw action_id asTaskContext.actionId, so new threads are taggedmetadata.{taskId,actionId}(taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys offmetadata.taskId) and the conversation list filters byactionId(+agent_id) — different actions of the same task get separate lists. xdfx's first-question opening text also appends行动id(action_id)为…(seebuildOpeningText/taskThreadMetadata). goPath→businessCode:3qfx→3Q,rwfx→6BF,xdfx→7BF. The roundtable target also threads the URLtaskIdintoroundtablePrefill.taskId(resolveTaskDeeplinkTarget(goPath, detail, taskId)), which turns on two taskId-scoped behaviors: (1) separate, unpartitioned per-task storage — taskId 会商聊天记录 live in their own store/table (roundtable_task_drafts), keyed only bytask_id(one taskId = many records, shared across users, no user 分权 at all;created_byis audit-only), kept entirely out of the per-user personal store (roundtable_drafts, now strictly per-user) so the two never mix. Backend router/api/roundtable-task-drafts(GET /by-task/{id}+/by-task/{id}/latest,POST,GET/PUT/DELETE /{id}— all user-agnostic); frontend clientroundtable-planning/api/task-drafts.ts, anduseDraftPersistence/useRoundtableDraftsroute all CRUD to it when ataskIdis present (else the personaldrafts.ts). The matching background jobs are shared by task too:roundtable_jobs.task_idmarks a task job; reads/dedup/stream/resume/cancel pass?task_id=so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over byscripts/migrate_roundtable_task_drafts.py(migration20260618_01). The history dropdown filters to the task and 新建 keeps the binding; (2) Step-3 auto structured output — after theroundtable-summaryagent writes the Markdown report it automatically runs a second round emitting aflow-jsonflowchart (useStep3SummaryautoFlow+chainLevels), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). Both targets render embedded under the 任务管理 layout — nested routes undertask-management/<business-path>/{roundtable,agent/:agent_id/chats}inStrategyRoutes.tsx(<business-path>=plan-task-sentiment/plan-task/plan-task-fx/plan-task/plan-task-action, matching the COH中任务 menu paths insidebar-menu.ts) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. All three single-agent businesses shareTaskFxWorkspace(task-components/TaskFxWorkspace.tsx): left = a「当前任务」card (readCopTaskDetail()任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (useTaskThreads(taskId, agentId, actionId?)— filtered by bothmetadata.taskIdANDmetadata.agent_idso the three businesses' conversations stay isolated even though they share one taskId; xdfx passes a thirdactionIdso it filters bymetadata.actionIdtoo, partitioning chat records per 行动 —useChatsBasederives it fromTaskContext.partitionByAction) + 新建对话; the…/chatsindex (TaskFxIndexResolver) opens the latest task thread or 新建. The right:thread_idhost differs by business: rwfx →TaskFxChat(AgentChatPage taskGate) = full workspace (non-lite): 目的树闸门 panel + 清除记录 (placed under 新建对话); xdfx → alsoTaskFxChat(same 目的树闸门 query+display, samepurpose_detail_url) but workspace passedlite; 3qfx →TaskBindChat(AgentChatPage taskBind,lite) = 1:N list only, no gate, and 新建对话 auto-sends the task opening text (taskAutoSendText; gate businesses send it via the gate's 开始问答 instead). rwfx auto-starts the first Q&A when the 任务目的 query returns empty (TaskPurposePanel autoStartWhenEmpty, threaded from the rwfxTaskFxChatonly). The 目的树闸门 (TaskPurposePanel) defaults collapsed and now keeps only 弹框查看 + 开始问答; the former jump buttons (编辑/下一步/3q详情/切换三情/弹框查看) all moved to the configurable bottom 快捷跳转 bar (TaskJumpBar, red 红底白字 buttons, right-aligned) driven by 按钮管理 — see the 按钮管理 bullet.TaskPurposeDialog(exported fromTaskPurposePanel) backs both the gate zoom and the bottom bar'spurpose-type button.chatBasePathkeeps the post-send URL rewrite + new-chat button in the task-management context. Backend configtask_deeplink(config.yaml, surfaced byGET /api/public/config/task-deeplink, mirrored incore/auth/task-deeplink-config.ts) addsaction_test/action_detail_url/detail_3q_url+mock.action_detail. Any failure (no consumer URL / fetch error / unmapped goPath / unresolvable action id / no configured agent) silently falls back to the default home. Helpers:core/auth/task-deeplink.ts,core/auth/task-purpose.ts,core/auth/task-deeplink-config.ts,core/threads/use-task-threads.ts. -
agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制): goPath=
agentfx(${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue)。与 3qfx 的差异:①任务详情不走外部 consumer,改调本系统 GatewayGET {backend}/taskAnalyseSearch/cop-task-detail?taskId=(apiFetch带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②固定使用内置智能体agentfx-analyst(不走业务映射);③路由task-management/plan-task-agentfx/agent/:agent_id/chats,页面为独立复制的AgentFxWorkspace.tsx(AgentFxWorkspace/AgentFxIndexResolver/AgentFxChat—— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,AgentChatPage taskBind自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块core/auth/agentfx-deeplink.ts(isAgentfxGoPath/fetchAgentfxTaskDetail/resolveAgentfxDeeplinkTarget);任务详情整包仍存共享键login.copTaskDetail(按标签页隔离),AgentChatPage的 taskBind 自恢复对 agentfx 生效。LoginPage在既有深链分支前用isAgentfxGoPath分流。内置「任务研判报告助手」:后端 seederapp/gateway/routers/_agentfx_seed.py+_agentfx_seed_assets/(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在_sync_legacy_agents之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(web_search,首步不可跳过)→ 每次检索 JSON 原样落盘/mnt/user-data/workspace/hits/qN.json→ 一条命令task-report-build(groupall)技能函数转换成 14 类task-reports.json(模型禁止手写该 JSON;分页技能task-report-{enemy,our,env,judge}仅用于事后补跑单页;五者都是共享引擎task-report-lib/convert_lib.py的瘦 CLI,resolve_hit_files对虚拟绝对路径读空的情况自动回退到相对写法并在warnings里列出尝试过的路径,免得智能体自己写脚本排查)→write_file生成/mnt/user-data/outputs/report.md→ 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁\uXXXX转义、每类必须有实质内容不许空数组/纯占位、文件一律用/mnt/user-data/...虚拟路径(防宿主路径泄漏进 tool args)。检索依赖config.yaml tools.web_search(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)。任务工作区输入框在taskCtxOn时隐藏「写作/记忆/参考文献」(InputBoxhideContextToolsprop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(artifactDisplayName收敛宿主/虚拟路径)。入库两条路径:(a) 前端协助卡 ——AgentChatPage(taskBind 且非 taskGate)检测线程 artifacts 出现task-reports.json(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入additional_kwargs.task_report_import_approval虚拟 ai 消息(core/messages/utils.ts分组assistant:task-report-import+MessageList的taskReportImportSlot插槽渲染TaskReportImportCard),点「开始入库」前端校验(strategy-components/lib/agentfx-report-schema.ts,14 类 + contentJson 合法 JSON + \u 转义归一化还原为汉字,与技能脚本同规则)后importTaskReport(strategy-components/api/task-reports.ts)POST 导入接口整体替换;任务已有详情不抑制卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) 入库技能task-report-import(seeder 一并复制到skills/public/,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑scripts/task_report_import.py(纯标准库:同样的校验规则 + 转义归一化;默认关闭 SSL 证书校验ssl._create_unverified_context+ 忽略代理,适配内网自签证书;鉴权--token/envAGENTFX_IMPORT_TOKEN→DEERFLOW_AUTH_TOKEN→TOOLS_TOKEN优先,缺省经公共端点POST /api/parallel-agents/auth/token以agentfx-import用户现签 token——内网既有鉴权旁路;gateway 地址 envAGENTFX_GATEWAY_URL或http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT});--strict可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的missing_categories里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。入库成功通知宿主(iframe 嵌入场景):两条路径成功后都会window.top.postMessage({source: "magent-web", type: "task-report:saved", taskId: String(taskId)}, "*")(notifyTaskReportSaved,agentfx-report-schema.ts)——卡片路径在TaskReportImportCard导入成功回调里直发;技能路径由AgentChatPage从消息流检测task_report_import.py的成功 stdout("success": true+task_id)代发——仅在回合结束后检测(!thread.isLoading才扫描;流式过程中 messages 每增量全量扫描会卡死页面),只看线程内最后一次成功入库(新一次入库触发新通知;刷新旧对话按 thread 记 sessionStorage 已通知的消息 id 不重复发)。 -
iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商) — 三个按标签页开关,全部由深链 URL 参数开启、存进同一份
sessionStorage会话(embed.session,键名常量在core/embed/embed-session.ts),跨登录跳转/刷新/页内导航存活,绝不跨标签污染同浏览器另开的正常系统页。核心隔离手法:用sessionStorage(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非localStorage(跨标签共享)。开关:(1)?embed=1(可带&theme=) — 去全局 chrome(PageLayout的pageSidebarAllowed && !isEmbedActive()→ 隐藏 Header + PageSidebar)+ 按外部主题色强制明/暗:走 next-themes 的forcedTheme(App.tsxAppProviders调resolveForcedEmbedTheme,不写 localStoragestrategy-theme),离散映射dark-blue/dark→深、其余→浅;嵌入侧绝不调setTheme/persistSidebarEnabled(那俩落 localStorage 跨标签污染,LoginPage据readEmbedParam跳过)。(2)?hideJump=1— 隐藏深链底部「快捷跳转」栏TaskJumpBar的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,isJumpBarHidden()为真时整条return null。(3) 登录用户隔离 — 嵌入态下鉴权/身份键(deerflow.authtoken、userInfo、login.*、tools_token、maxkey/userDetail token)全部经core/auth/scoped-storage.ts的authStorage()(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户完全隔离、互不覆盖;所有 token 读取都过getStoredAuth()/getAuthorizationHeaderValue()(API 客户端core/api/*全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听AuthSyncWatcher(core/auth/sync.tsx)在嵌入态禁用。嵌入会话在main.tsx渲染前syncEmbedSessionFromCurrentUrl()同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);AppProviders的 effect 再按 SPA 导航同步。embed=0/hideJump=0可显式关闭。正常(非嵌入)标签页所有isXxx()恒 false,行为零变化。第三个开关?isolate=1(独立登录会话)把同一套登录用户隔离单独提供给普通(带 chrome)标签页——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:authStorage()的条件从「仅嵌入」放宽为isEmbedActive() || isIsolatedSession(),AuthSyncWatcher的跨标签监听对嵌入态或独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,设置 → 外观 →「登录会话」 有一个 Switch「独立登录会话(本标签页)」运行时切换(setIsolatedSession,appearance-settings-page.tsx):开启时先migrateAuthToSession()(scoped-storage.ts,把 localStorage 的受保护键deerflow.auth/userInfo/userId/tools_token+login.*前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。 -
Web App 产物模式(多页面站点 / 流程图 / 地图,默认关闭、全链路可降级): 让智能体产出完整站点而非单个 xxx.html。沙箱技能
skills/public/web-app用模板(backend/templates/web-app-template,经config.yaml sandbox.mounts只读挂载到/mnt/web-app-template,目录缺失自动跳过)脚手架到outputs/<site>/(site.json 清单 +src/pages/*.tsx文件名即路由 +public/数据),再跑scripts/build_site.py build用预置 node_modules(symlink/junction 链接,离线永不 npm install)执行 vite build 出dist/;内置组件FlowCanvas(@xyflow/react)/GeoMap(MapLibre,默认无底图 GeoJSON 模式,site.json 可配 tilesUrl/PMTiles)/ChartPanel(ECharts)/MermaidBlock。平台侧app/gateway/routers/web_app_sites.py+ 纯引擎deerflow/webapp/(发现/子进程构建/HMAC 签名):GET /api/web-app/capability(enabled/模板/依赖/node 四项探测)、GET|POST /api/threads/{tid}/sites[...]{build,build-status,preview-url},静态托管走GET /api/public/site-preview/{token}/{path}(/api/public/免鉴权命名空间,唯一允许 HTML inline 的出口——签名令牌 + dist/ 根限定 + 按请求真实 origin 显式声明的 CSP(iframe sandbox 不透明 origin 下 'self' 失效)+ nosniff;artifacts 接口既有的强制 attachment 不变)。前端:独立风格预览工作台pages/site-studio/SiteStudioPage.tsx(顶层路由#/sites/:threadId/:siteName,App.tsx注册,不进 PageLayout/WorkspaceLayout;自带 site-studio.css 深色设计体系,零全局样式污染;iframesandbox="allow-scripts allow-forms allow-popups allow-modals"无 allow-same-origin)+ 产物面板artifact-file-detail.tsx里 site.json 的「站点预览」按钮(capability 可用才渲染,唯一既有文件改动)。降级铁律:默认完全关闭(sandbox.mounts的模板挂载条目与文件末尾的web_app:段在 config.yaml 里均为注释状态);开启需同时放开两处(挂载让沙箱技能可探测/mnt/web-app-template,web_app.enabled: true开平台 API——只开一处会得到"生成了看不了"或"看不了也不生成"的中间态)。build_site.py check在宿主侧会读 config.yaml 的web_app.enabled(envDEERFLOW_WEBAPP_ENABLED/DEERFLOW_WEBAPP_CONFIG可覆盖;沙箱内读不到配置时"挂载即视为开")。任何一环(配置/模板/node_modules/node)缺失 → capability 不可用 + 技能第 0 步探测失败即回退单页 HTML 模式 + 入口按钮不渲染 + 构建失败只收进任务状态,现有功能零影响。配置web_app:(enabled/template_dir/build_timeout_seconds/preview_token_ttl_seconds/node_path);测试tests/test_web_app_sites.py;开发机make webapp-template预置依赖,离线发版由 deerflow-offline-deployment 仓库在 CI 预置 node_modules 进镜像。
Configuration
Backend config lives in offline-backend-20260512/backend/config.yaml. Values starting with $ resolve as environment variables (e.g., $OPENAI_API_KEY). MCP servers and skills are configured in extensions_config.json in the same directory.
Detailed Backend Reference
The backend has its own comprehensive documentation at:
offline-backend-20260512/backend/AGENTS.md— architecture, middleware chain, all API routes, config schema, sandbox/memory/subagent systemsoffline-backend-20260512/backend/README.md— setup guide, technology stack