deerflow-code/frontend-web/docs/前端开发.md
2026-09-07 18:24:55 +08:00

69 KiB
Raw Permalink Blame History

前端开发文档(frontend-web)

本文档面向参与 frontend-web/ 开发的工程师,覆盖工程结构、技术栈、环境与启动、路由与页面、状态体系、与后端 LangGraph 的实时流式交互、上传/产物(Artifacts)、模型与设置、鉴权、主题与国际化、构建与发布等所有关键开发点,力求从零到一帮助新成员快速上手。


1. 概览

  • 项目名:deer-flow-frontend-vite(package.json)
  • 类型:单页应用(SPA),React 19 + TypeScript 5 + Vite 6
  • 包管理器:pnpm(packageManager: pnpm@10.26.2)
  • 路由:react-router-dom v7(采用 HashRouter)
  • 状态:TanStack Query(服务端状态)+ React Context(流式会话/Artifacts/Subtasks/PromptInput)+ localStorage/sessionStorage(用户偏好与流式恢复)
  • UI 体系:TailwindCSS v4 + Radix UI 原语 + shadcn 风格自封装(src/components/ui/)+ TDesign(少量页面)+ Lucide 图标
  • LLM 流式交互:@langchain/langgraph-sdk,与后端 Gateway 上的 LangGraph 运行时(默认 lead_agent)建立 SSE 连接
  • 渲染:react-markdown + streamdown + rehype-* + shiki/react-syntax-highlighter + katex
  • 富文本:@tiptap/*(用于 Open Canvas 写作)
  • 编辑器:@uiw/react-codemirror(代码片段编辑)
  • 流程图:@xyflow/react(节点连线类组件)

这是一个由 Next.js 历史代码迁移而来的 Vite 工程:保留 next/link、next/navigation、use client 等遗留写法,并通过 src/shims/ 提供运行时兼容。新开发中可以继续按 Next.js 习惯写 useRouter()/useParams()/usePathname(),由 shim 转译为 react-router 实现。


2. 目录结构

frontend-web/
├─ index.html                       入口模板(含黑/白主题快速注入脚本)
├─ vite.config.ts                   Vite 配置(alias + sourceIdentifier 插件)
├─ tsconfig.json                    TS 编译选项
├─ postcss.config.js                PostCSS + tailwindcss v4
├─ .env / .env.development / .env.production    后端地址等环境变量
├─ docs/                            前端文档(本目录)
└─ src/
   ├─ main.tsx                      React 入口(StrictMode + 全局样式)
   ├─ App.tsx                       根组件:HashRouter + 顶层 Provider 嵌套
   ├─ env.ts                        读 import.meta.env(兼容旧 NEXT_PUBLIC_ 命名)
   ├─ styles/globals.css            全局 + Tailwind 入口
   ├─ shims/                        next/link、next/navigation 的 react-router 适配
   ├─ pages/                        路由页面(HashRouter 下的所有 Route 元素)
   │  ├─ PageRoutes.tsx             /page/* 一级布局:左侧 PageSidebar + Header
   │  ├─ WorkspaceRoutes.tsx        /page/workspace/* 二级路由(聊天/Agents/Skills/Memory/Admin…)
   │  ├─ StrategyRoutes.tsx         /page/strategy/* 策略页
   │  ├─ RoundtableRoutes.tsx       /page/roundtable/*
   │  ├─ ChatPage.tsx               主聊天页(绑定 lead_agent)
   │  ├─ AgentChatPage.tsx          基于自定义 Agent 的会话
   │  ├─ ScheduledTasksPage.tsx     定时任务
   │  ├─ ScheduledTaskRunDetailPage.tsx
   │  ├─ PublicSharePage.tsx        免登录分享只读
   │  ├─ PublicScheduledTaskPage.tsx
   │  ├─ EmbedChatPage.tsx          iframe 嵌入问答(见 docs/embed-iframe-chat.md)
   │  ├─ LoginPage.tsx              用户名直登(开发/内网用)
   │  └─ ...
   ├─ core/                         「领域核心」: API 客户端、领域 hooks、类型
   │  ├─ api/                       langgraph-sdk client + 鉴权 fetch
   │  ├─ auth/                      访问令牌存取(localStorage)
   │  ├─ config/                    后端/LangGraph URL 解析
   │  ├─ threads/                   会话流(核心 useThreadStream)
   │  ├─ uploads/                   线程上传/校验/解析
   │  ├─ settings/                  本地偏好与线程级模型覆盖
   │  ├─ tasks/                     子任务 SubtaskContext
   │  ├─ models/                    可用模型 + 用量开关
   │  ├─ agents/                    自定义 Agent CRUD + 选择器
   │  ├─ skills/、memory/、tags/    其他领域
   │  ├─ scheduled-tasks/           定时任务
   │  ├─ artifacts/                 产物(代码/文档)读取
   │  ├─ recommended-questions/、notifications/、admin/、curator/、tool-metrics/、llm-metrics/
   │  ├─ note/                      轻应用(笔记)API(独立后端 VITE_NOTE_API_BASE_URL)
   │  ├─ i18n/                      语言上下文、词条、检测
   │  ├─ rehype/、streamdown/、tools/、messages/、page-layout/、route-base.tsx
   │  └─ utils/                     uuid、json、files、markdown、datetime
   ├─ contexts/                     全局 React 上下文(Chat/Theme)
   ├─ components/
   │  ├─ ui/                        shadcn 风格基础组件(button、dialog、sidebar、resizable…)
   │  ├─ ai-elements/               AI 业务组件(prompt-input、conversation、message、plan、reasoning…)
   │  ├─ workspace/                 工作台模块(chats、messages、artifacts、settings、admin、agents…)
   │  ├─ landing/                   营销页(Landing)
   │  ├─ query-client-provider.tsx  TanStack Query 容器
   │  └─ theme-provider.tsx         next-themes 容器
   ├─ open-canvas/                  开放画布/AI 写作(独立路由:/page/canvas/*)
   ├─ roundtable-planning/          圆桌规划(独立路由:/page/roundtable/*)
   ├─ strategy-components/          策略相关组件 + Header
   ├─ hooks/                        通用工具 hooks(debounce、async、localStorage…)
   └─ lib/                          通用工具:cn、desensitize、ime

3. 启动、构建、脚本

3.1 NPM Scripts(package.json)

脚本 命令 说明
pnpm dev vite 启动开发服务器(默认端口 5173;项目实际通过仓库根脚本统一为 5174)
pnpm build node --max-old-space-size=8192 vite build 生产构建(提升 Node 内存阈值,避免大型依赖 OOM)
pnpm preview vite preview 本地预览 dist 构建产物
pnpm typecheck tsc --noEmit 仅做 TS 类型检查,不输出文件

3.2 仓库根脚本

scripts/start-frontend.sh(与后端联动)会自动注入 VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 与 VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api,并将日志/PID 写入 .runtime/。详细命令见根目录 CLAUDE.md:

./scripts/start-all.sh    # 同时启动 backend + frontend
./scripts/stop-all.sh
./scripts/restart-all.sh
./scripts/status.sh

3.3 Vite 别名(vite.config.ts)

别名 真实路径
@/ src/
@vite/ src/
@/env src/env.ts
next/link src/shims/next-link.tsx
next/navigation src/shims/next-navigation.ts

工程同时配置了 vite-plugin-source-identifier:构建产物会带源文件标记,便于线上排错对应到源文件。

base: "./" 表示打包后使用相对路径,方便部署到任意子路径或本地 file:// 调试。

3.4 浏览器与构建目标

  • TS target: ES2022、module: ESNext、moduleResolution: Bundler
  • useDefineForClassFields: true、isolatedModules: true、jsx: react-jsx
  • 严格模式开启,但 noImplicitAny: false

4. 环境变量

入口在 src/env.ts,所有公共变量使用 VITE_ 前缀,运行时通过 import.meta.env 读取。出于兼容旧 Next.js 代码,导出的对象保留 NEXT_PUBLIC_ 命名:

env.NEXT_PUBLIC_BACKEND_BASE_URL          // VITE_BACKEND_BASE_URL
env.NEXT_PUBLIC_LANGGRAPH_BASE_URL        // VITE_LANGGRAPH_BASE_URL
env.NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL // 画布独立 LangGraph(可选,默认回退主地址)
env.NEXT_PUBLIC_NOTE_API_BASE_URL         // 轻应用「笔记」API
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY       // 静态演示模式(禁用上传/发送)
env.GITHUB_OAUTH_TOKEN                    // 备用:landing 中拉取仓库信息

.env 文件加载优先级(Vite 标准):

.env.development.local > .env.development > .env (开发)
.env.production.local > .env.production > .env (生产)

个人本机覆盖请使用 .env.development.local(已 gitignore)。

getBackendBaseURL() / getLangGraphBaseURL() / getCanvasLangGraphBaseURL() 均在 src/core/config/index.ts 中,会基于当前 window.location.origin 兜底为同源 /api/langgraph。


5. 渲染入口与 Provider 体系

5.1 启动栈

ReactDOM.createRoot(#root)
└─ <React.StrictMode>
   └─ <App>
      └─ <HashRouter>
         └─ <AppProviders>
            ├─ <NextThemesProvider>          // next-themes,attribute=class,存储键 "strategy-theme"
            │   ├─ <I18nProvider>            // 语言上下文(locale)
            │   │   └─ <ThemeProvider>       // 自定义主题(与 next-themes 协作)
            │   │       └─ <ChatProvider>    // QA 历史(localStorage: qa-chat-history)
            │   │           └─ <QueryClientProvider>  // TanStack Query
            │   │               └─ <Routes>...

index.html 头部内联了一段脚本,会在 React 启动前根据 localStorage["strategy-theme"] 决定是否给 <html> 添加 dark 类,避免暗色主题闪烁。

5.2 路由

外层(App.tsx):

/                       -> Redirect to /login
/login                  LoginPage
/login/:username        LoginPage(按用户名直登)
/landing                LandingPage
/share/:shareCode       PublicSharePage(免登录只读)
/embed/chats/:thread_id EmbedChatPage(iframe 嵌入问答,见 docs/embed-iframe-chat.md)
/public/scheduled-tasks/:taskId   PublicScheduledTaskPage(免登录)
/page/*                 PageRoutes(含侧边栏与 Header)
*                       NotFoundPage

/page/* 内(PageRoutes.tsx):

/page/workspace/*  -> WorkspaceRoutes(默认)
/page/strategy/*   -> StrategyRoutes
/page/canvas/*     -> open-canvas/routes/OpenCanvasRoutes
/page/roundtable/* -> RoundtableRoutes

/page/workspace/*(WorkspaceRoutes.tsx):

chats                  -> Navigate to chats/new
chats/:thread_id       -> <WorkspaceLayout><ChatRuntime><ChatPage>
agents                 -> AgentsPage
agents/new             -> NewAgentPage
agents/:agent_id/chats/:thread_id -> AgentChatPage
skills                 -> SkillsPage
skills/evolution       -> SkillEvolutionPage
memory                 -> MemoryPage
ai-writing             -> AIWritingPage(来自 open-canvas)
roundtable/planning    -> RoundtablePlanningPage
scheduled/tasks        -> ScheduledTasksPage
scheduled/runs/:run_id -> ScheduledTaskRunDetailPage
admin                  -> AdminUserHubPage (system_role=admin)
admin/scheduled-tasks  -> AdminScheduledTasksPage (admin)
admin/tags             -> TagsPage (admin)
settings/recommended-questions    (admin)
settings/prompt-prefix            (admin)
settings/llm-metrics              (admin)

WorkspaceRoutes 通过 <RouteBaseProvider value="/page/workspace"> 提供基址上下文,让兼容自 Next.js 的 useRouter().push("/page/workspace/...") 在不同的二级路由下都能映射到正确的 base。

重要约定:使用 HashRouter,URL 形如 /#/page/workspace/chats/new。这是为了便于在静态文件部署(包括打包后 file://)下使用。新建一条聊天时,前端会用 history.replaceState(...) 直接改写 hash,而不让 router 重渲染整棵子树 —— 这一点在 ChatPage.tsx:75-79 中体现,依赖了 useThreadChat() 的特殊兜底逻辑(避免 useParams 把 new 误判为真实 thread_id)。

5.3 Workspace 双层布局

  • <WorkspaceLayout> 提供 <SidebarProvider> + <WorkspaceSidebar> + <SidebarInset> + <CommandPalette> + <Toaster>,并通过 CSS 变量 --page-sidebar-width 把内层侧栏紧贴外层 PageSidebar。
  • 对于「聊天类」路由 (chats/:thread_id、agents/.../chats/:thread_id),额外包一层 <ChatRuntime>:
<SubtasksProvider>
  <ArtifactsProvider>
    <PromptInputProvider>
      {children}
    </PromptInputProvider>
  </ArtifactsProvider>
</SubtasksProvider>

这三个上下文只在聊天会话内生效,避免污染列表/管理类页面。


6. 鉴权

6.1 登录流程

src/pages/LoginPage.tsx 通过 loginByUsername(username) 直接向后端发起登录(默认 guest),返回结构存在 src/core/auth/index.ts:

interface LoginByUsernameResponse {
  access_token: string;
  token_type: string;        // 默认 "Bearer"
  expires_in: number;
  user_id: string;
  email: string;
  system_role: string;        // "admin" 控制管理员菜单
  needs_setup: boolean;
  created: boolean;
}

存储键:localStorage["deerflow.auth"]。

工具方法:

  • getStoredAuth() / setStoredAuth(auth) / clearStoredAuth()
  • getAccessToken() —— 返回 token 字符串
  • getAuthorizationHeaderValue() —— 返回 "Bearer xxx",缺失时 undefined

6.2 鉴权注入

所有出站请求都通过两条路径之一注入 Token:

  1. 裸 fetch —— 使用 src/core/api/fetch-client.ts 提供的 apiFetch(input, init),自动在 headers 上加 Authorization。
  2. LangGraph SDK —— src/core/api/api-client.ts 在 new LangGraphClient 的 onRequest 钩子里塞 Authorization,并对 runs.stream / runs.joinStream 做 sanitizeRunStreamOptions 包装(见 core/api/stream-mode.ts),确保流式参数合法。

务必使用 apiFetch 而不是裸 fetch,否则鉴权头不会带上,私有接口会 401。

6.3 路由级守护

WorkspaceRoutes 内的 /admin/* 与 /settings/* 路由在元素层面做了判定:

getStoredAuth()?.system_role === "admin"
  ? <WorkspaceLayout>...</WorkspaceLayout>
  : <Navigate to="/page/workspace/chats/new" replace />

非管理员被静默重定向到聊天页。


7. 与后端的通信

7.1 三种入口

通道 函数/客户端 用途
LangGraph SDK getAPIClient(isMock?) LangGraph runtime:threads.search/get/update/delete,runs.stream/joinStream
HTTP fetch apiFetch(url, init) Gateway 业务接口(Agents/Memory/Skills/Tags/Uploads/Scheduled-tasks…)
独立服务 getNoteApiBaseURL() 轻应用「笔记」服务

7.2 LangGraph 客户端缓存

getAPIClient(isMock?) 通过模块级 Map 进行客户端实例缓存(default / mock),避免重复构造。

7.3 流式聊天的核心:useThreadStream

定义于 src/core/threads/hooks.ts。这是「整套前端的心脏」,所有聊天页都基于它:

const [thread, sendMessage, isUploading] = useThreadStream({
  threadId,
  context,                  // LocalSettings["context"]
  isMock,
  assistantId: "lead_agent",
  onStart(threadId) { /* 第一次创建会话时回调,用于改写 URL */ },
  onFinish(state)   { /* 流式收束,浏览器后台时弹通知 */ },
  onToolEnd(evt)    { /* 工具调用结束钩子 */ },
});

关键能力:

  1. 断线重连:通过 sessionStorage["lg:stream:<threadId>"] 存储正在进行的 run_id,刷新页面后 SDK 自动 joinStream 续接。normalizeStoredRunId() 容错处理历史 URL/带 query 的情况。
  2. 乐观更新:发送即在本地 messages 后追加临时 human 消息;当文件正在上传时还追加占位 ai 消息("uploadingFiles");服务端有响应(messages 数量增加)后自动清空。
  3. 首条消息特例:仅当线程已持久化时立即触发 onStart;新会话需要等到 SDK onCreated(meta.thread_id),确保 URL 同步用的是真正的服务端 thread_id。
  4. Context 装配:每次提交时把 thinking_enabled / is_plan_mode / subagent_enabled / reasoning_effort / memory_injection_enabled / thread_id 等基于「四种 mode」推导:
mode thinking_enabled is_plan_mode subagent_enabled reasoning_effort
flash false false false undefined
thinking true false false low
pro true true false medium
ultra true true true high
  1. 附件上传:先 uploadFiles(threadId, files) 上传到 Gateway /api/threads/:threadId/uploads,然后将 path/filename/size 写入 messages[0].additional_kwargs.files,与 lead_agent 后端约定一致。
  2. TanStack Query 失效:流式完成或新建会话时调用 queryClient.invalidateQueries({ queryKey: ["threads", "search"] }),让侧边栏「最近会话」自动刷新。
  3. 错误降级:onError 弹 sonner toast;getStreamErrorMessage(error) 兜底解析对象/嵌套字段。

7.4 重要回调事件

SDK 事件 处理
onCreated(meta) 新线程创建:触发 onStart、把 agent_id 写入 thread metadata(非 mock)
onLangChainEvent 监听 on_tool_end 并外抛 onToolEnd
onUpdateEvent 若 update.title 出现,则更新 TanStack Query 缓存里的 thread 标题
onCustomEvent task_running:更新 SubtaskContext;llm_retry:弹 toast 提示
onError 清空乐观消息,弹 toast
onFinish 触发外部 onFinish + 刷新 threads.search

7.5 其他领域 hook 约定

所有领域目录(agents/、skills/、memory/、tags/、scheduled-tasks/、notifications/、admin/、curator/、llm-metrics/、tool-metrics/、system-settings/、thread-shares/、recommended-questions/)遵循统一模板:

core/<domain>/
  ├─ api.ts       底层调用(fetch + apiFetch)
  ├─ hooks.ts     useQuery/useMutation 包装
  ├─ types.ts     接口类型
  └─ index.ts     re-export

写新领域时也按此模板维持一致。


8. 全局状态与本地存储

8.1 React Context(运行时状态)

Context 文件 作用域 状态
ChatContext contexts/ChatContext.tsx App 顶层 通用 QA 消息历史(localStorage: qa-chat-history)
ThemeContext contexts/ThemeContext.tsx App 顶层 主题,与 next-themes 协作
I18nContext core/i18n/context.tsx App 顶层 当前语言(Cookie: locale)
SubtaskContext core/tasks/context.tsx ChatRuntime 子任务最新 message
ArtifactsContext components/workspace/artifacts/context.tsx ChatRuntime 当前会话产物列表与选中
PromptInputContext components/ai-elements/prompt-input.tsx ChatRuntime 输入框附件/状态
RouteBaseContext core/route-base.tsx Workspace/Strategy/Roundtable 给 useRouter shim 提供 base prefix
ThreadContext components/workspace/messages/context.ts 每个聊天页内 共享 thread 与 isMock 给 MessageList 等子组件

8.2 LocalSettings(用户偏好)

core/settings/local.ts + core/settings/store.ts + core/settings/hooks.ts:

LocalSettings = {
  notification: { enabled: boolean },
  display:      { streamSpeedMeterEnabled: boolean },
  context: {
    model_name?: string,
    mode?: "flash" | "thinking" | "pro" | "ultra",
    reasoning_effort?: "minimal"|"low"|"medium"|"high",
    writing_mode?: boolean,
    memory_injection_enabled?: boolean,
    agent_id?: string,
  }
}
  • 持久化:localStorage["deerflow.local-settings"]
  • 线程级模型覆盖:localStorage["deerflow.thread-model.<threadId>"],通过 useThreadSettings(threadId) 自动合并
  • 实现:自定义的「订阅 + storage 事件」store,借助 useSyncExternalStore 让多标签页/多组件同步

8.3 sessionStorage

Key 写入位置 说明
lg:stream:<threadId> useThreadStream(SDK 内部) 当前 run_id,用于 reconnect
pendingAgentMessage ChatPage 提交时(含 agentId) 跳转 AgentChatPage 后续发首条消息

9. 组件层次

9.1 src/components/ui/

shadcn 风格的「无业务」基础组件:button、badge、avatar、card、carousel、command、dialog、dropdown-menu、hover-card、input、input-group、item、progress、resizable、scroll-area、select、separator、sidebar、skeleton、sonner、switch、tabs、textarea、toggle、toggle-group、tooltip 等。

特色组件:

  • aurora-text / flickering-grid / magic-bento / spotlight-card / shine-border / confetti-button —— Landing 视觉效果
  • terminal —— 终端样式
  • number-ticker / word-rotate —— 动画文本(基于 motion)

9.2 src/components/ai-elements/

AI 业务可复用组件,是聊天页的「乐高」:

组件 用途
prompt-input 输入区域容器、附件、提交按钮、模式选择
conversation 消息流容器(自动滚动到底,依赖 use-stick-to-bottom)
message / message-content 消息气泡
reasoning 折叠的「思考」区段
plan Plan-Mode 任务计划
chain-of-thought 步骤链路
task 单步任务展示
checkpoint 流程节点
sources 来源引用列表
suggestion 建议追问/快捷动作
code-block 代码块(shiki/syntax highlighter)
model-selector 模型下拉
web-preview / image 网页/图片预览
canvas / node / edge / controls xyflow 流程图原语
loader / shimmer / toolbar / panel / queue 辅助 UI
open-in-chat 「在聊天中打开」入口

9.3 src/components/workspace/

工作台模块,承载 90% 业务页面:

workspace/
├─ workspace-sidebar.tsx       右栏导航
├─ workspace-header.tsx        顶部
├─ workspace-nav-menu.tsx
├─ workspace-nav-chat-list.tsx 最近会话
├─ workspace-container.tsx     业务包裹器
├─ command-palette.tsx         ⌘K
├─ thread-title.tsx
├─ welcome.tsx / agent-welcome.tsx
├─ todo-list.tsx
├─ token-usage-indicator.tsx
├─ streaming-indicator.tsx
├─ input-box.tsx               基于 PromptInput 的实际输入框(模式、Agent、附件)
├─ export-trigger.tsx          导出会话 docx/md
├─ thread-share-dialogs.tsx    创建/管理只读分享链接
├─ recent-chat-list.tsx / recent-writing-list.tsx
├─ chats/
│   ├─ chat-box.tsx            左右双栏 (聊天 + Artifacts),react-resizable-panels
│   ├─ use-thread-chat.ts      解析 :thread_id(new -> uuid)
│   └─ use-chat-mode.ts        特定 mode hook
├─ messages/
│   ├─ message-list.tsx        渲染所有消息、groupMessages
│   ├─ message-group.tsx
│   ├─ message-list-item.tsx
│   ├─ markdown-content.tsx    react-markdown + rehype-katex + rehype-raw + remark-gfm + remark-math
│   ├─ subtask-card.tsx
│   ├─ message-token-usage.tsx
│   ├─ use-message-metrics.ts
│   ├─ download-word-button.tsx
│   ├─ context.ts              ThreadContext
│   └─ skeleton.tsx
├─ artifacts/
│   ├─ context.tsx             ArtifactsContext + Provider
│   ├─ artifact-file-list.tsx
│   ├─ artifact-file-detail.tsx
│   └─ artifact-trigger.tsx    顶部「打开 Artifacts」按钮
├─ citations/                  引用链接渲染
├─ settings/                   设置弹窗与子页(appearance/account/skill/memory/tool/about)
├─ admin/                      管理员页(user-management / tag-management / admin-user-hub / tool-metrics / memory)
├─ agents/                     Agent gallery、selector、manage dialog
├─ skills/                     skill-detail-dialog、skill-evolution-panel
├─ scheduled-tasks/            schedule-builder
├─ tags/                       tag-picker-dialog、tag-chip、tag-search-bar、tag-colors
└─ ...

9.4 src/open-canvas/

AI 写作 / 画布编辑器(基于 @tiptap/*):

  • routes/OpenCanvasRoutes.tsx 二级路由
  • pages/:CanvasHomePage / CanvasThreadPage / AIWritingPage / ArticleTypesConfigPage
  • editor/TiptapEditor.tsx + TiptapExtensions.ts + AiHighlightExtension.ts
  • contexts/:Canvas 自有的 Stream/Thread/Graph/UI Context(与主聊天页解耦)
  • hooks/:useCanvasStream、useCanvasArtifact、useCanvasThread、useWritingRewrite、useDocumentOutline 等
  • api/:自己封装 /canvas/runs /canvas/threads /rewrite /writing-export /ai-writing-sessions
  • 独立 LangGraph 服务地址通过 VITE_CANVAS_LANGGRAPH_BASE_URL 配置;未配置则与主聊天同源(仅 assistantId 不同)

9.5 src/roundtable-planning/

圆桌规划页(多 Agent 协同生成方案):

  • pages/RoundtablePlanningPage.tsx 主页
  • components/:Step1/Step2/Step3 面板、MessageBubble、PersonaDrawer、HighFidelityReport、RecommendAgentsDialog
  • hooks/:useStep1Intent、useStep2Orchestration、useAutoScroll、useDraftPersistence
  • api/:intent / multi-agent / recommend
  • lib/:clarification 规则、reasoning 工具、step-display
  • 自带 styles/roundtable-planning.css

9.6 src/strategy-components/

策略类页面(含 QA 中心 / Agent QA / Collaborative QA / MopeChat),其中部分仍保留 .jsx 旧文件。新增功能优先迁移到 .tsx。


10. 样式与主题

10.1 TailwindCSS v4

  • postcss.config.js 引入 @tailwindcss/postcss
  • 入口样式:src/styles/globals.css
  • 配合 tw-animate-css 提供常用动效
  • cn(...) 工具(src/lib/utils.ts)= twMerge(clsx(...)),是合并 className 的「事实标准」

10.2 主题

  • 顶层用 next-themes,attribute="class",storageKey="strategy-theme",默认 light,/landing 强制 light
  • 自定义 ThemeProvider(contexts/ThemeContext.tsx)暴露 theme 给少量组件做白底 sidebar 等微调(见 workspace-sidebar.tsx:27)
  • index.html 早期注入脚本读取 storage,避免 FOUC

10.3 暗色样式

  • .dark 类位于 <html>,Tailwind v4 通过 darkMode: class(约定)匹配

11. 国际化(i18n)

src/core/i18n/:

  • locale.ts 提供 detectLocale(),按 navigator 推断
  • translations.ts / locales/index.ts 汇总词条
  • context.tsx 暴露 I18nProvider + useI18nContext()
  • hooks.ts 提供 useI18n(),返回 { t, locale, setLocale }
  • 切换语言写 Cookie(locale=xxx),下次访问加载对应词条

业务里固定写法:

const { t } = useI18n();
return <span>{t.common.loading}</span>;

12. 路由层的 Next.js 兼容

src/shims/next-navigation.ts 通过 react-router-dom 实现了:

  • useRouter():push/replace/back/forward/refresh/prefetch(其中 push/replace 会将旧 base /page/workspace 替换成 RouteBaseContext 值)
  • usePathname():useLocation().pathname
  • useParams<T>():原样转发
  • useSearchParams():只返回 searchParams(注意:不是 tuple,与 Next.js 习惯一致)

src/shims/next-link.tsx 把 <Link href=...> 桥接到 <a>/<NavLink>。

新代码可继续使用 next/link、next/navigation,但请避免依赖 SSR/数据获取等 Next.js 高阶特性。


13. 渲染与 Markdown

src/components/workspace/messages/markdown-content.tsx 使用 react-markdown + 以下插件:

  • remark-gfm —— GFM 表格、删除线
  • remark-math + rehype-katex —— 数学公式
  • rehype-raw —— 渲染内嵌 HTML
  • core/rehype 中的 useRehypeSplitWordsIntoSpans():流式追加时按词包 <span>,配合 CSS 渐入动画
  • core/streamdown —— 流式 Markdown 解析(streamdown 包)
  • 代码块用 shiki(高亮)+ react-syntax-highlighter(兜底)

引用渲染:components/workspace/citations/citation-link.tsx 和 artifact-link.tsx 将 [[uuid]] 风格引用转成可悬浮的链接卡片。


14. 上传与产物(Artifacts)

14.1 上传

  • uploadFiles(threadId, files):POST 到 /api/threads/:threadId/uploads(FormData,files 字段重复多份)
  • 大小/类型/数量校验:core/uploads/file-validation.ts
  • PromptInput 附件解析:core/uploads/prompt-input-files.ts 提供 promptInputFilePartToFile(),把组件 UI Part 转为 File
  • 失败容错:useThreadStream 中若有任意 file 转换失败,会抛出 toast 并清掉乐观消息

14.2 产物

  • 服务端在 thread.values.artifacts: string[] 里报路径
  • ArtifactsProvider(components/workspace/artifacts/context.tsx)做三层状态:所有产物、选中产物、面板开关
  • ChatBox 用 react-resizable-panels 拉出右侧 40% 面板,展示 ArtifactFileList / ArtifactFileDetail
  • 选中 Markdown 产物时,「写作模式」会把 writing_artifact_path 注入 context(ChatPage.tsx:157-159),后端 PromptPrefix/Writing 中间件会接管

15. 模型/模式/Token 用量

15.1 useModels

core/models/hooks.ts:

const { models, tokenUsageEnabled, isLoading } = useModels();
  • loadModels() 调 Gateway /api/models(具体见 core/models/api.ts)
  • tokenUsageEnabled 控制顶部 <TokenUsageIndicator> 是否显示

15.2 模式 (mode)

flash | thinking | pro | ultra 四档(见 §7.3 表),由 <InputBox> 中的 mode 切换器修改 LocalSettings.context.mode。ModeHoverGuide 提供悬浮说明。

15.3 Token 用量


16. 通知、快捷键、命令面板


17. 定时任务、分享

17.1 定时任务

17.2 只读分享

  • 创建分享:components/workspace/thread-share-dialogs.tsx + core/thread-shares/
  • 只读页 PublicSharePage(路由 /share/:shareCode)/ PublicScheduledTaskPage(/public/scheduled-tasks/:taskId),不需要登录
  • iframe 嵌入问答 EmbedChatPage(/embed/chats/:thread_id):第三方用普通 iframe 嵌套,父页面用 sessionId 轮询公开 API 判断问答是否结束,详见 embed-iframe-chat.md
  • 后端会用 share token 做权限校验,前端通过 apiFetch 自动带

18. 调试与测试

18.1 类型检查

pnpm typecheck

工程没有内置 ESLint/Prettier 配置(依赖 IDE/CI),但有少量 // eslint-disable-next-line react-hooks/exhaustive-deps 注释提示 dependencies 故意省略。

18.2 Mock 模式

URL 加 ?mock=true 时,useThreadChat() 设 isMock=true,getAPIClient(true) 使用 mock 客户端,getLangGraphBaseURL(true) 回退到 ${origin}/mock/api,便于离线复现 UI(需要后端或本地 mock server 配合)。

18.3 演示模式

VITE_STATIC_WEBSITE_ONLY=true 时:

  • 输入框 disabled,提示「演示模式不可用」
  • 自动展开 Artifacts 面板并选中第一个产物
  • 用于把构建产物嵌入营销页 / 静态主页

18.4 在浏览器调试 SSE

LangGraph SDK 走标准 SSE。开发期通过 Chrome DevTools Network -> EventSource 可以直接查看 frames。失败原因(如 422)可在 sonner toast 看到 getStreamErrorMessage() 的解析结果。


19. 常见开发任务指南

19.1 新增一个聊天页内的子组件

  1. 在 src/components/workspace/ 下建目录与 index.ts
  2. 通过 useThread()(来自 messages/context.ts)拿到当前 thread
  3. 业务展示用 <MessageList> / <MarkdownContent> 现有组件,避免自行 Markdown 解析
  4. 若依赖产物,使用 useArtifacts()

19.2 新增一个 LangGraph Assistant(自定义 Agent)

  1. 后端注册新的 graph
  2. 前端 core/agents/api.ts 增 CRUD 接口;hooks.ts 增 useAgents/useAgent/useCreateAgent
  3. 路由:复用 AgentChatPage,URL /page/workspace/agents/:agent_id/chats/:thread_id
  4. 提交时只需把 assistantId 传给 useThreadStream(参考 AgentChatPage 中传 agent_id 给后端 graph 的方式)

19.3 新增一个领域 API

  1. 在 core/<domain>/ 建 api.ts/hooks.ts/types.ts/index.ts
  2. api.ts 函数全部使用 apiFetch(${getBackendBaseURL()}/api/...)
  3. hooks.ts 用 useQuery({ queryKey: ["<domain>", ...] }) / useMutation({ onSuccess: () => invalidateQueries })
  4. 在使用方 import useXxx 即可

19.4 新增一个全局快捷键

useGlobalShortcuts([
  { key: "k", meta: true, action: openCommandPalette },
  { key: "/", meta: false, shift: true, action: showHelp },
]);

19.5 修改本地偏好

const [settings, setSettings] = useLocalSettings();
setSettings("display", { streamSpeedMeterEnabled: false });

线程级:

const [settings, setSettings] = useThreadSettings(threadId);
setSettings("context", { model_name: "gpt-5" }); // 仅当前线程

19.6 新增 i18n 词条

  1. 在 core/i18n/translations.ts 与 locales/ 下补充
  2. useI18n().t.xxx.yyy 使用
  3. 不要硬编码中英文文案到组件里

20. 性能与稳定性约定

  • TanStack Query:键统一以「领域 + 操作 + params」组合(如 ["threads", "search", params]、["agents", id]);mutate 之后 invalidate;写时支持 setQueriesData 立即生效更新缓存(参考 useDeleteThread/useRenameThread)
  • 流式断线续连:保持 useThreadStream 的 reconnectOnMount 启用(已默认);切换线程时会 reset runMetadataStorageRef 以确保不串
  • 乐观更新:发送 message 立即出现 human 气泡,避免空白等待;附件场景额外提供 uploadingFiles 占位
  • 避免重复发送:sendInFlightRef 在 useThreadStream 里做单飞锁
  • 不阻塞主线程:长文 markdown 拆词、渲染时分批;use-stick-to-bottom 自动暂停跟随
  • 大依赖懒加载:建议对低频路由(如 Open Canvas、RoundtablePlanning)使用 React.lazy + Suspense(当前已大致按模块切分,文件级可继续优化)
  • 构建内存:pnpm build 已 --max-old-space-size=8192,新增大依赖请关注 dist 体积与构建时长

21. 部署

  • 默认构建产物 dist/,因 base: "./" 可直接置于任何静态文件路径
  • HashRouter 不需要后端 fallback,但建议把所有未匹配请求回退到 index.html,便于直接访问深链
  • 后端地址通过环境变量在 build-time 注入(Vite 把 VITE_* 内联到 bundle 中),不同环境请构建不同产物或使用窗口注入的方式
  • 内网部署时常配合 Nginx 反代 /api -> 后端 Gateway、/api/langgraph -> LangGraph Server,让前端使用同源相对路径无需额外 CORS

22. 已知历史包袱与注意事项

  1. HashRouter + replaceState:新建线程时 history.replaceState 直接改写 hash,不再触发 router 渲染。若以后改用 BrowserRouter,需要重写此处(参考 ChatPage.tsx:75-79 与 chats/use-thread-chat.ts 的兜底)
  2. "use client" 指令:迁移自 Next.js,在 Vite 中无意义但保留可读性,新文件可省略
  3. Next.js shim:尽量使用 useRouteNavigate()(core/route-base.tsx)代替原始的 useRouter().push("/page/workspace/..."),因为后者依赖 base 替换,与你所在的路由模块强相关
  4. 混合 jsx/tsx:strategy-components/** 内仍有 .jsx,新增请直接用 .tsx 并补全类型
  5. TDesign:仅在少量页面被使用,避免与 shadcn UI 风格混用;优先复用 components/ui/
  6. Open Canvas / Roundtable:各自有独立的 Stream/Thread Context 与 API;不要直接在它们里复用主聊天的 useThreadStream,反之亦然
  7. 生产模式默认是 light,但 index.html 早期脚本默认 dark:如果改默认主题,要同时改 App.tsx:29 的 defaultTheme 和 index.html 的脚本

23. 速查表

我想… 去哪里看 / 怎么写
改后端地址 .env.development[.local] 中的 VITE_BACKEND_BASE_URL / VITE_LANGGRAPH_BASE_URL
新增路由 pages/WorkspaceRoutes.tsx(工作台)或 pages/PageRoutes.tsx(一级)
发起 GET/POST apiFetch(${getBackendBaseURL()}/api/..., { method, body })
发起 SSE 聊天 useThreadStream({ threadId, context, ... })
跨标签同步偏好 useLocalSettings() / useThreadSettings(threadId)
弹 toast import { toast } from "sonner"; toast.success("done")
弹浏览器通知 useNotification().showNotification(title, { body })
国际化 const { t } = useI18n(); t.common.loading
合并 className cn("base", cond && "x-y")
获取登录态 getStoredAuth() / getAccessToken()
命令面板 ⌘K 唤起 <CommandPalette/>

24. 后续建议

  • 统一目录命名:workspace/messages/、workspace/artifacts/、workspace/chats/ 现都用 kebab-case 文件夹 + kebab-case 文件名;建议在新增文件时延续此约定
  • 抽离配置常量:与服务端契约相关的硬编码(如 assistantId = "lead_agent")建议集中到 core/config/
  • 测试:当前缺单测;优先为 core/threads/hooks.ts 的 normalizeStoredRunId 等纯函数补 vitest
  • 包体积:landing 视觉特效(ogl、gsap、canvas-confetti、embla-carousel)较重,可考虑路由级 code-split
  • lint/format:建议引入 ESLint + Prettier + lint-staged,统一团队风格

25. 页面清单(Page Catalog)

本节按一级路径分组,列出 src/pages/、src/open-canvas/pages/ 与 src/roundtable-planning/pages/ 下的每一个页面:做什么 + 挂载在哪里 + 关键依赖与组件。

25.1 顶层路由(无 /page 前缀)

LandingPage —— /landing

LoginPage —— /login、/login/:username

  • 文件:LoginPage.tsx
  • 作用:以用户名直登(默认 guest),从 query 解析 pageSidebarEnabled 偏好,登录成功后写入 localStorage["deerflow.auth"] 并跳到 /page/workspace/chats/new
  • 关键依赖:loginByUsername、setStoredAuth、persistSidebarEnabled
  • UI:极简卡片(Tailwind),仅在 error 时显示失败原因

PublicSharePage —— /share/:shareCode

  • 文件:PublicSharePage.tsx
  • 作用:通过分享码免登录查看一段会话快照(含产物面板)
  • 数据:getPublicShareSnapshot
  • 内部布局:自建左右双栏(不依赖 WorkspaceLayout)
  • 自带 <ArtifactsProvider> + <SubtasksProvider> + <ThreadContext.Provider>,用快照模拟 BaseStream<AgentThreadState>

PublicScheduledTaskPage —— /public/scheduled-tasks/:taskId

NotFoundPage —— *

  • 文件:NotFoundPage.tsx
  • 作用:404 兜底,给一个 "Open workspace" 链接回到 /page/workspace/chats/new

25.2 /page/workspace/*(工作台主区)

所有页面外层套 <WorkspaceLayout>(侧栏 + 内容区)。聊天类页面再多一层 <ChatRuntime>(SubtasksProvider/ArtifactsProvider/PromptInputProvider)。

ChatsPage —— /page/workspace/chats

ChatPage —— /page/workspace/chats/:thread_id

AgentChatPage —— /page/workspace/agents/:agent_id/chats/:thread_id

  • 文件:AgentChatPage.tsx
  • 作用:与自定义 Agent 对话。assistantId=agent_id,context 注入 agent_id。会消费 sessionStorage["pendingAgentMessage"] 来自动发送 ChatPage 转交的首条消息
  • Hook:与 ChatPage 类似,但额外 useAgent(agent_id)
  • 关键组件:与 ChatPage 同一套;顶栏额外渲染 Agent 名称 Badge;落地态使用 AgentWelcome

AgentsPage —— /page/workspace/agents

NewAgentPage —— /page/workspace/agents/new

  • 文件:NewAgentPage.tsx
  • 作用:以「对话」的方式引导用户创建 Agent —— 复用 lead_agent 的 is_bootstrap=true 模式 + SetupAgentApprovalMiddleware 中拦截的 setup_agent 工具调用,让 LLM 给出 setup_agent 调用并由用户点「批准」最终落库
  • Hook:useThreadStream + useSkills + useModels + checkAgentName/createAgent/updateAgent
  • 组件:
    • 左:表单(名称/描述/灵魂 prompt/模型/技能多选)
    • 右:<PromptInput> + <MessageList>(带 SetupAgentApprovalPayload 审批 UI)
    • SkillDetailDialog
    • 包了 <ArtifactsProvider> + <ThreadContext.Provider> 与 ChatPage 同源

ScheduledTasksPage —— /page/workspace/scheduled/tasks

ScheduledTaskRunDetailPage —— /page/workspace/scheduled/runs/:run_id

  • 文件:ScheduledTaskRunDetailPage.tsx
  • 作用:单次定时任务运行的详情;展示消息时间线、产物文件、状态、可重命名/删除/导出 docx
  • Hook:useScheduledTaskRun、useScheduledTaskRunMessages、useScheduledTaskActions、getScheduledTaskRunFileContent、updateScheduledTaskRunFile、downloadScheduledTaskRunFile
  • 组件:Tabs、Card、MarkdownContent、Textarea(编辑结果文件)

ScheduledChatPage(未在路由中显式注册,但留作入口跳转)

  • 文件:ScheduledChatPage.tsx
  • 作用:从 useSchedulerThread() 拿到「调度器助手」线程 id 后立即 Navigate 到 /page/workspace/chats/<id>,相当于一个智能跳板
  • 组件:WorkspaceContainer/Body + Loading 提示文案

SkillsPage —— /page/workspace/skills

SkillEvolutionPage —— /page/workspace/skills/evolution

MemoryPage —— /page/workspace/memory

AIWritingPage —— /page/workspace/ai-writing

  • 文件:open-canvas/pages/AIWritingPage.tsx(带 hideAdvancedFields)
  • 作用:AI 写作主界面(左草稿/中聊天/右时间线),WorkspaceLayout 内嵌入
  • 见 §25.4 详解

RoundtablePlanningPage —— /page/workspace/roundtable/planning

25.3 管理员页(/page/workspace/admin/* 与 settings/*)

路由元素层做了 getStoredAuth()?.system_role === "admin" 判断,非管理员被重定向到 chats/new。

AdminUserHubPage —— /page/workspace/admin

  • 文件:AdminUserHubPage.tsx(薄壳)→ AdminUserHub
  • 作用:统一的「用户中心」聚合页(用户列表、活动、记忆查看),替代旧的多分支管理路由
  • 旧路径 admin/users / admin/user-activity / admin/memory 均 Navigate 到此页

AdminScheduledTasksPage —— /page/workspace/admin/scheduled-tasks

AdminUserActivityPage(路由已并入 AdminUserHub,但文件保留)

AdminMemoryPage(已并入 Hub)

UsersPage(已并入 Hub)

TagsPage —— /page/workspace/admin/tags

LlmMetricsPage —— /page/workspace/settings/llm-metrics

RecommendedQuestionsPage —— /page/workspace/settings/recommended-questions

PromptPrefixSettingsPage —— /page/workspace/settings/prompt-prefix

25.4 /page/canvas/*(开放画布 / AI 写作)

由 OpenCanvasRoutes.tsx 注册,不在 WorkspaceLayout 内(直接挂在 PageRoutes 内容区)。

CanvasHomePage —— /page/canvas

CanvasThreadPage —— /page/canvas/:thread_id

AIWritingPage —— /page/canvas/ai-writing 与 /page/workspace/ai-writing

ArticleTypesConfigPage —— /page/canvas/article-types

25.5 /page/roundtable/*(圆桌规划)

RoundtablePlanningPage —— /page/roundtable/planning

25.6 /page/strategy/*(策略路由)

StrategyRoutes.tsx 复用了 WorkspaceRoutes 内大多数页面,不带 WorkspaceLayout/PageSidebar,由 SidebarProvider 单独维持。<RouteBaseProvider value="/page/strategy"> 让 useRouter 跳转保持在策略前缀下。

25.7 路由与页面对照速查

一级路径 进入路由文件 主要页面文件
/login、/login/:username App.tsx LoginPage.tsx
/landing App.tsx LandingPage.tsx
/share/:shareCode App.tsx PublicSharePage.tsx
/public/scheduled-tasks/:taskId App.tsx PublicScheduledTaskPage.tsx
/embed/chats/:thread_id App.tsx EmbedChatPage.tsx(embed-iframe-chat.md)
/page/workspace/* PageRoutes.tsx → WorkspaceRoutes.tsx ChatsPage / ChatPage / AgentsPage / NewAgentPage / AgentChatPage / SkillsPage / SkillEvolutionPage / MemoryPage / ScheduledTasksPage / ScheduledTaskRunDetailPage / AdminUserHubPage / AdminScheduledTasksPage / TagsPage / RecommendedQuestionsPage / PromptPrefixSettingsPage / LlmMetricsPage
/page/strategy/* PageRoutes.tsx → StrategyRoutes.tsx GeneralQA + LightApp* + 复用 Workspace 页面
/page/canvas/* PageRoutes.tsx → open-canvas/routes/OpenCanvasRoutes.tsx CanvasHomePage / CanvasThreadPage / AIWritingPage / ArticleTypesConfigPage
/page/roundtable/* PageRoutes.tsx → RoundtableRoutes.tsx RoundtablePlanningPage
* App.tsx NotFoundPage

25.8 "薄壳页面"与"重逻辑页面"

页面 类型 真正实现位置
AgentsPage 薄壳 components/workspace/agents/agent-gallery.tsx
AdminUserHubPage 薄壳 components/workspace/admin/admin-user-hub.tsx
AdminMemoryPage 薄壳 components/workspace/admin/admin-memory-page.tsx
UsersPage 薄壳 components/workspace/admin/user-management-page.tsx
TagsPage 薄壳 components/workspace/admin/tag-management-page.tsx
SkillsPage 薄壳 components/workspace/settings/skill-settings-page.tsx
SkillEvolutionPage 薄壳 components/workspace/skills/skill-evolution-panel.tsx
MemoryPage 薄壳 components/workspace/settings/memory-settings-page.tsx
其余 重逻辑 直接在 pages/ 内实现

维护建议:薄壳页面只负责 layout/insets,业务逻辑请始终下沉到 components/workspace/<domain>/ 下方便复用与测试。


本文档与代码同步演进。若发现内容与实现不符,请优先以代码为准并更新本文。