- 3qfx/xdfx 单智能体改走任务工作区(TaskFxWorkspace):按 taskId+agent_id 隔离的 1:N 对话列表;3qfx 纯绑定(taskBind 自动发开场信息),xdfx 带目的树闸门(taskGate) - 左上角「当前任务」卡片:留存查询到的任务名称+简介(readCopTaskDetail) - rwfx 工作区新增「切换到三情分析」按钮,带同一 taskId 进入 3qfx - xdfx 深链传入的是行动id:resolveDeeplinkTaskId 经 action 接口解析出真正任务id (可配置 action_detail_url + action_test/mock.action_detail,离线可测) - 目的树面板「弹框查看」旁新增可配置「3q详情」链接(detail_3q_url + ?id=任务id) - AgentChatPage 拆分 taskGate/taskBind;use-task-threads 增加 agentId 过滤隔离 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.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/.
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/CLAUDE.md for the full ordered list.
Harness → App import firewall is enforced by tests/test_harness_boundary.py (runs in CI).
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) - 轻应用中心 (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-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 ofcomponents/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 incore/page-layout/menu-overrides.ts(applyMenuOverrides, applied inuse-sidebar-menu.tsafter light-app injection, before role filter; disabling a node hides its whole subtree) — strict 3-level cap (wouldExceedDepth). Clientstrategy-components/api/menu-overrides.ts. Backend:/api/menu-overrides(global/shared; open read, admin atomic replace-all). - 敏感词管理 (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. - 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). 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) per-task draft isolation — drafts save/list/restore bytask_id(one taskId = many records, shared across users; backendGET /api/roundtable-drafts/by-task/{id}+/latest); 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)— filtered by bothmetadata.taskIdANDmetadata.agent_idso the three businesses' conversations stay isolated even though they share one taskId) + 新建对话; 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 + 编辑/下一步/清除记录 task actions + a「切换到三情分析」button (carries the same taskId into 3qfx viaresolveTaskDeeplinkTarget("3qfx", …)); xdfx → alsoTaskFxChat(same 目的树闸门 query+display, samepurpose_detail_url) but workspace passedlite(no task-actions/jump in the left panel); 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). The shared 目的树闸门 (TaskPurposePanel) carries a「3q详情」link beside「弹框查看」(buildDetail3qUrl→detail_3q_url+?id=任务id, opens new tab; toasts 未配置 when the URL is blank — it's TBD).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.
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/CLAUDE.md— architecture, middleware chain, all API routes, config schema, sandbox/memory/subagent systemsoffline-backend-20260512/backend/README.md— setup guide, technology stack