deerflow-code/AGENTS.md
2026-09-07 18:24:55 +08:00

38 KiB
Raw Blame History

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 from app.*.
  • 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 in src/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 「问答管理 → 课题研究(对话版)」. Page open-canvas/pages/AIWritingChatPage.tsx reuses the engine (AIWritingContext + useStream + AIWritingDraftPanel); idle keeps the classic setup layout (WritingSetupChat + collapsible form) and switches to the chat workbench once writing starts; left panel components/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. Tests tests/test_ai_writing_intent.py; plan doc frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md.
  • 轻应用中心 (Light App Center): a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers window_open or iframe mini-apps with a route-param builder: login params resolved from localStorage.userInfo, theme params literal) and 应用列表 (card grid of window_open apps, window.open launch). iframe apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and dynamically injected into the sidebar by core/page-layout/use-sidebar-menu.ts (useSidebarMenuItems), opening in the LightAppIframeView host. Client + React Query hooks in strategy-components/api/light-apps.ts; URL builder/constants in strategy-components/lib/light-app.ts. Backend: /api/light-apps (global/shared, admin-gated writes).
  • 菜单管理 (Menu Management, admin-only): an overlay over the static sidebar menu — MenuManagementPage (pages/MenuManagementPage.tsx, route /page/strategy/admin/menu-management, opened from the admin-only 「菜单管理」 item in the bottom-left settings dropdown of components/page-sidebar.tsx). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag or the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, including injected iframe light-app nodes (light-app-<appId>). The static tree (core/page-layout/sidebar-menu.ts) stays the source of icons/routes; only per-node deltas (label/disabled/parentId/sortOrder) persist to the backend. Merge logic in core/page-layout/menu-overrides.ts (applyMenuOverrides, applied in use-sidebar-menu.ts after light-app injection, before role filter; disabling a node hides its whole subtree) — strict 3-level cap (wouldExceedDepth). Client strategy-components/api/menu-overrides.ts. Backend: /api/menu-overrides (global/shared; open read, admin atomic replace-all). 简介模式 uses a separate overlay: every compact workspace menu item can be placed in sidebar(左侧菜单)、settings(设置和更多)or toolbox(右侧工具箱); CompactToolbox (components/workspace/compact-toolbox.tsx, mounted by PageRoutes.tsx) opens only when the square right-edge trigger is clicked, then slides a compact, content-height right-side drawer over a backdrop above global dropdowns; configured items render top-to-bottom as centered square cards with a large icon above the label while the effective layout is compact. /api/compact-menu-overrides stores two independent sets (type A / B); each website picks one via runtime-config.js VITE_COMPACT_MENU_TYPE.
  • 舆情分析智能体(前端虚拟智能体): 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 pages/SentimentAgentChatPage.tsx(路由 agents/sentiment-analysis/chats/:thread_id)复用普通问答布局 + MessageList,输入框只有发送按钮;问答经 Gateway POST /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 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage deerflow.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(同一 TDesign Guide 封装模式,与简洁模式引导共享记忆语义:仅显式点「完成/跳过」才写 localStorage deerflow.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, else user_id, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. src/lib/desensitize.ts ships a frozen seed map SEED_DESENSITIZE_MAP (the deployment seed) plus a live mutable DESENSITIZE_MAP (initialized from the seed so desensitize() works before any fetch). DesensitizeWordsLoader (mounted in App.tsx AppProviders) fetches /api/sensitive-words on startup and, if non-empty, calls replaceDesensitizeMap(...) to make the backend config the source of truth (empty/failed → keep seed). Page SensitiveWordsPage.tsx (route /page/strategy/admin/sensitive-words, opened from the 「敏感词管理」 item in the bottom-left settings dropdown of components/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 missing SEED_DESENSITIZE_MAP entries into the editor. Client + hooks in strategy-components/api/sensitive-words.ts (wordsToMap folds enabled rows into the runtime map). Backend: /api/sensitive-words (global/shared; open read, write allowed for admin or the lqq account), mirroring 菜单管理's store shape. 侧边栏 + 导航栏 menu labels are desensitized at the data source: useSidebarMenuItems/useActiveThirdLevel (core/page-layout/use-sidebar-menu.ts) map every node's label through a desensitizer built directly from the /api/sensitive-words query (makeDesensitizer(wordsToMap(words)), falling back to the global seed desensitize while empty) — this sidesteps the replaceDesensitizeMap effect-timing race. Only label is rewritten; path/id/icon are 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; MenuManagementPage uses the raw tree so admins still edit real labels.
  • 技能地址管理 (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,router app/gateway/routers/skill_addresses.py,tests tests/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}; linkType is url (外链 window.open, optional append of ?{idParam}={id} — idParam is the editable query-param name, e.g. id/task_id), business (内部切换到另一业务, reuses resolveTaskDeeplinkTarget, always task id), or purpose (弹出当前任务的「任务目的」表格 via the shared TaskPurposeDialog, no target — configurable for any page incl. xdfx). host(宿主路由) asks the embedding parent to navigate: runTaskButton sends window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin) — target is the host route, appendTaskId/idParam become numeric queryParams (task id only), origin from runtime-config.js VITE_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) — TaskContext now also stashes the raw actionId, so a url button can append either (default task). Page TaskButtonsPage.tsx (route /page/strategy/admin/task-buttons, admin-gated, entry「按钮管理」in the bottom-left settings dropdown of components/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 to defaultTaskButtons(cfg) — rwfx's url buttons (编辑/下一步/3q详情) built from the live /api/public/config/task-deeplink plus 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: TaskFxWorkspace no longer hardcodes 切换到三情分析, TaskActionsPanel keeps only the non-jump 清除记录, and TaskPurposePanel dropped its 3q详情 button (now configurable) and defaults the 目的树 table to collapsed (chevron toggle). Helpers core/auth/task-buttons.ts (TaskButton, defaultTaskButtons, resolveTaskButtons, buttonsForBusiness, runTaskButton), client+hooks strategy-components/api/task-buttons.ts. Backend: /api/task-buttons (global/shared; open read, admin replace-all), store deerflow.persistence.task_buttons (table task_buttons, auto-created by create_all), wired app.state.task_button_store. Tests: tests/test_task_buttons.py.
  • State: TanStack Query for server state, context providers (SubtasksProvider, ArtifactsProvider, PromptInputProvider) scoped to WorkspaceRoutes
  • LLM streaming: @langchain/langgraph-sdk streams messages from the backend via SSE
  • UI: Radix UI primitives + TailwindCSS v4 + shadcn-style components in src/components/ui/
  • Shims: src/shims/ provides next/link and next/navigation polyfills (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 API GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId= (header Authorization: Bearer admin, auto-injected by utils/request.ts), stashes the full data (id/overview/taskDirection/taskName/taskContent) into sessionStorage["login.copTaskDetail"], then routes by goPath: base codes 3qfx/rwfx/xdfx → that business's configured single agent Q&A inside a 任务工作区 (resolved via /api/business-mapping; resolveTaskDeeplinkTarget calls persistTaskContext({taskId, agentId, openingText}) and navigates to the …/chats index, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT pendingAgentMessage); the -A suffix (3qfx-A/…) → multi-agent roundtable with roundtablePrefill.businessCode so the page auto-selects the configured 业务链条. xdfx's deep-link ?taskId= is actually an 行动id, not a 任务id — resolveDeeplinkTaskId(goPath, rawId) (called in LoginPage before everything) detects xdfx/xdfx-A and resolves it via GET {action_detail_url}/{actionId} → data.taskId (fetchTaskIdFromAction; action_test+mock.action_detail for offline testing); the resolved 任务id then drives cop-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: resolveTaskDeeplinkTarget sets partitionByAction:true + keeps the raw action_id as TaskContext.actionId, so new threads are tagged metadata.{taskId,actionId} (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off metadata.taskId) and the conversation list filters by actionId (+agent_id) — different actions of the same task get separate lists. xdfx's first-question opening text also appends 行动id(action_id)为… (see buildOpeningText / taskThreadMetadata). goPath→businessCode: 3qfx→3Q, rwfx→6BF, xdfx→7BF. The roundtable target also threads the URL taskId into roundtablePrefill.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 by task_id (one taskId = many records, shared across users, no user 分权 at all; created_by is 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 client roundtable-planning/api/task-drafts.ts, and useDraftPersistence/useRoundtableDrafts route all CRUD to it when a taskId is present (else the personal drafts.ts). The matching background jobs are shared by task too: roundtable_jobs.task_id marks 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 by scripts/migrate_roundtable_task_drafts.py (migration 20260618_01). The history dropdown filters to the task and 新建 keeps the binding; (2) Step-3 auto structured output — after the roundtable-summary agent writes the Markdown report it automatically runs a second round emitting a flow-json flowchart (useStep3Summary autoFlow+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 under task-management/<business-path>/{roundtable,agent/:agent_id/chats} in StrategyRoutes.tsx (<business-path> = plan-task-sentiment / plan-task/plan-task-fx / plan-task/plan-task-action, matching the COH中任务 menu paths in sidebar-menu.ts) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. All three single-agent businesses share TaskFxWorkspace (task-components/TaskFxWorkspace.tsx): left = a「当前任务」card (readCopTaskDetail() 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (useTaskThreads(taskId, agentId, actionId?) — filtered by both metadata.taskId AND metadata.agent_id so the three businesses' conversations stay isolated even though they share one taskId; xdfx passes a third actionId so it filters by metadata.actionId too, partitioning chat records per 行动 — useChatsBase derives it from TaskContext.partitionByAction) + 新建对话; the …/chats index (TaskFxIndexResolver) opens the latest task thread or 新建. The right :thread_id host differs by business: rwfx → TaskFxChat (AgentChatPage taskGate) = full workspace (non-lite): 目的树闸门 panel + 清除记录 (placed under 新建对话); xdfx → also TaskFxChat (same 目的树闸门 query+display, same purpose_detail_url) but workspace passed lite; 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 rwfx TaskFxChat only). 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 from TaskPurposePanel) backs both the gate zoom and the bottom bar's purpose-type button. chatBasePath keeps the post-send URL rewrite + new-chat button in the task-management context. Backend config task_deeplink (config.yaml, surfaced by GET /api/public/config/task-deeplink, mirrored in core/auth/task-deeplink-config.ts) adds action_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,改调本系统 Gateway GET {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 分流。内置「任务研判报告助手」:后端 seeder app/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(group all)技能函数转换成 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 时隐藏「写作/记忆/参考文献」(InputBox hideContextTools prop)与「知识空间」(不传 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/env AGENTFX_IMPORT_TOKEN→DEERFLOW_AUTH_TOKEN→TOOLS_TOKEN 优先,缺省经公共端点 POST /api/parallel-agents/auth/token 以 agentfx-import 用户现签 token——内网既有鉴权旁路;gateway 地址 env AGENTFX_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.tsx AppProviders 调 resolveForcedEmbedTheme,不写 localStorage strategy-theme),离散映射 dark-blue/dark→深、其余→浅;嵌入侧绝不调 setTheme/persistSidebarEnabled(那俩落 localStorage 跨标签污染,LoginPage 据 readEmbedParam 跳过)。(2) ?hideJump=1 — 隐藏深链底部「快捷跳转」栏 TaskJumpBar 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,isJumpBarHidden() 为真时整条 return null。(3) 登录用户隔离 — 嵌入态下鉴权/身份键(deerflow.auth token、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 提示。

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 systems
  • offline-backend-20260512/backend/README.md — setup guide, technology stack