Skip to content

前端技术栈与架构

本章目标:

  1. 看懂 a-cdm 前端的技术选型(Next.js 16 + React 19 + Tailwind 4 + pnpm)以及它为什么是 App Router 而非传统 SPA
  2. 建立 src/core(业务逻辑)/ src/components(视图)/ app(路由)三层心智地图,知道改业务该去哪个目录
  3. 彻底弄清 basePath=/a-cdm编译期静态替换——为什么改它必须重 build,而不能靠 Caddy 热切

TL;DR

a-cdm 前端是把 deer-flow 原生聊天前端深度二开后的 Next.js 16 应用:app/ 跑 App Router(41 个 page,核心是 /workspace/*),src/core/ 装纯业务逻辑(API client / hooks / 类型),src/components/ 装视图(ui/ 是 Shadcn 生成物,workspace/ 172 个业务组件)。所有 a-cdm 定制的业务客户端集中在 src/core/acdm/(27 个文件,经 index.ts 桶导出),走 /api/acdm 同域 + HttpOnly cookie。最关键的运维约束是 basePath=/a-cdm:它在 pnpm build 时被静态烤进每一个 <Link>/_next/static 路径,改它只能重 build,Caddy 运行时改不动。

Overview(为什么是这套栈,为什么 App Router)

a-cdm 前端要回答的问题是:一个有状态的 AI 协作工作台,前端怎么组织才不乱?

这不是一个普通表单网站。它要同时承载:① AI 流式对话(SSE 长连、可重连)② 项目/会议/报告等大量 CRUD 业务页 ③ 鉴权守卫(未登录跳 Keycloak)④ 富交互(日历、文件树、Markdown 渲染、Mermaid)。如果用纯 CSR 的 SPA,会撞上三个问题:

  1. 首屏鉴权时机:登录态校验若全在客户端做,未登录用户会先看到一闪而过的工作台空壳,再被踢走——体验差且有信息泄露风险。
  2. 路由即资源:/workspace/projects/[id]/meetings/[mid] 这种深层资源路由,手写客户端路由表会爆炸。
  3. 服务端能力缺失:鉴权 cookie 解析、SSR locale 探测、反向代理后端 API,都需要一个服务端运行时。

答案是 Next.js 16 App Router:服务端组件默认渲染、proxy.ts(Next.js 16 把 middleware.ts 重命名而来)做边缘守卫、文件系统即路由表、next.config.jsrewrites 充当 BFF 反代。a-cdm 没有重写这套底座,而是把 deer-flow 原生前端"撑开"——保留它的线程/流式机制,在 src/core/acdm/ 旁挂一整套业务域。React 19 + Tailwind 4 + pnpm 是 deer-flow 上游既定选型,a-cdm 沿用。

Architecture:三层分工 + 一份桶导出

前端代码遵循一条铁律:src/core/ 不含 JSX、src/components/ 不含跨页业务编排、app/ 只做路由装配。这条边界让"改业务逻辑"和"改界面"互不污染。

目录职责关键证据Source
路由层src/app/App Router,文件系统即路由,Server Component 默认41 个 page.tsx,核心 /workspace/*deer-flow/frontend/src/app/layout.tsx:23-36
业务逻辑层src/core/API client / TanStack hooks / 类型 / 流式状态,纯 TS 无视图26 个子模块,acdm/ 27 文件最大deer-flow/frontend/src/core/acdm/index.ts:1-13
视图层src/components/React 组件;ui/+ai-elements/ 为生成物只读workspace/ 172 文件,ui/ 48deer-flow/frontend/src/components/workspace/acdm
边缘层src/proxy.ts鉴权守卫 + dev auth header 注入matcher 限 /workspace/api/langgraph/api/memorydeer-flow/frontend/src/proxy.ts:92-99
配置层next.config.js / env.jsbasePath / rewrites / T3 env 校验basePath 来自 envdeer-flow/frontend/next.config.js:35

src/core/ 子模块按规模排序后,acdm/(27 文件)、i18n/(11)、api/(10)是三个最重的——acdm/ 是 a-cdm 全部定制业务的家,api/ 是 deer-flow 原生 LangGraph 接入层,i18n/ 是上游国际化。a-cdm 二开策略由此清晰:不动 deer-flow 原生 core/threadscore/api 的流式骨架,所有新业务进 core/acdm/

Components / Subsystems

App Router 路由结构

职责:文件系统即路由表,layout.tsx 嵌套构成 Provider 树。

根布局 app/layout.tsx(src/app/layout.tsx:23-36)是所有页面的外壳:它做 SSR locale 探测(detectLocaleServer)、挂 ThemeProvider + I18nProvider,并在此处声明 metadata title: "A-CDM"——这是 a-cdm 改的第一处定制痕迹(上游叫 DeerFlow)。

往下一层 app/workspace/layout.tsx(src/app/workspace/layout.tsx:18-39)是工作台外壳:从 sidebar_state cookie 读侧栏初始展开态(避免水合闪烁),挂 QueryClientProvider(TanStack Query)、SidebarProviderCommandPaletteToaster。再往下 app/workspace/projects/[id]/(workspace)/layout.tsxRoute Group (workspace)(括号目录不映射 URL)统一给所有项目子页套三栏布局:ProjectSubNav | 子页 | ProjectAgentChatPanel(src/app/workspace/projects/[id]/(workspace)/layout.tsx:1-21)。

41 个 page.tsx 的拓扑:/(landing)→ /workspace/*(主战场,含 chats / projects / agents / knowledge / admin / skills)→ /blog/[lang]/docs(Nextra 文档站)→ /mock/api/*(本地 mock 端点)。

路由段用途Source
app/workspace/chats/[thread_id]AI 对话(deer-flow 原生骨架)deer-flow/frontend/src/app/workspace/chats/[thread_id]/page.tsx
app/workspace/projects/[id]/(workspace)/*项目工作区(概览/文档/会议/报告/日历/WBS/知识库)deer-flow/frontend/src/app/workspace/projects/[id]/(workspace)/layout.tsx:1-30
app/workspace/admin/*后台(用户/审计/AI 角色/回收站)deer-flow/frontend/src/app/workspace/admin/layout.tsx
app/workspace/agents/*Agent 管理(新建/编辑/对话)deer-flow/frontend/src/app/workspace/agents/page.tsx
app/mock/api/*本地开发 mock(无后端时返假数据)deer-flow/frontend/src/app/mock/api/models/route.ts

src/core/acdm —— a-cdm 定制业务总线

职责:把 a-cdm 所有自研业务域的 HTTP client + TanStack hooks + 类型集中,经一个桶文件统一对外。

core/acdm/index.ts(src/core/acdm/index.ts:1-13)只做一件事——export * 把 13 个模块再导出。组件只需 import { projectApi, useMeetings } from "@/core/acdm",不关心内部拆成多少文件。这是典型的 barrel(桶)模式:对外一个稳定入口,对内自由重构。

核心是 core/acdm/api.ts(src/core/acdm/api.ts:62-97)的 request<T>() fetch 封装,所有业务 API 都走它:

ts
// 摘自 deer-flow/frontend/src/core/acdm/api.ts:62-97
export async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
  const resp = await fetch(`${ACDM_BASE}${path}`, {
    ...init, headers,
    credentials: "include",  // 带 HttpOnly cookie
  });
  if (resp.status === 401) { maybeRedirectToLogin(); throw new AcdmUnauthorized(); }
  // 非 JSON 5xx 兜底:resp.json() 直接炸会把真错误淹掉
  if (!resp.ok && !contentType.includes("application/json")) {
    throw new AcdmError(resp.status, `HTTP ${resp.status}: ${text.slice(0, 200)}`);
  }
  const json = (await resp.json()) as ApiResponse<T> & ...;
  if (json.code !== 0) throw new AcdmError(json.code, json.message, json.details);
  return json.data as T;
}

三个设计要点:① ACDM_BASE = "/api/acdm"同域相对路径,靠 next.config.js rewrite 转发,这样 HttpOnly cookie 才能被浏览器自动带回(src/core/acdm/api.ts:58-60);② 统一解 {code, message, data} 信封,code !== 0 一律抛 AcdmError;③ 401 自动 maybeRedirectToLogin()/api/acdm/auth/login,且用模块级 _redirectingToLogin 标志防重复跳(src/core/acdm/api.ts:110-117)。api.ts 之下按域拆成 ba-report-api.ts / knowledge-api.ts / wiki-api.ts,hooks 侧 hooks.ts / knowledge-hooks.ts / wiki-hooks.ts 用 TanStack Query 包一层(src/core/acdm/hooks.ts:1-23)。

src/components —— 生成物与业务组件分治

职责:视图层,把"自动生成、禁手改"的设计系统组件和"手写业务"组件物理隔离。

components.json(Shadcn 配置)声明 ui 别名指向 @/components/ui,registries 含 ai-elements / magicui / react-bits 三个远程源。这些目录是注册表生成物:eslint.config.js:10-13 明确把 src/components/ui/**src/components/ai-elements/** 加进 ignores——ESLint 不检查,也意味着不该手改(改了下次从 registry 重拉会被覆盖)。

业务组件全在 components/workspace/(172 文件),其中 components/workspace/acdm/ 是 a-cdm 定制视图(acdm-chat.tsx / CreateMeetingDialog.tsx / ba-report/ 等),与 core/acdm/ 一一呼应:core/acdm 出数据,components/workspace/acdm 出界面。

core/api —— deer-flow 原生 LangGraph 接入(只读骨架)

职责:LangGraph SDK 客户端单例 + 拦截器,a-cdm 不动其骨架只复用。

core/api/api-client.ts(src/core/api/api-client.ts:109-120)用 Map 缓存 client 单例(default / mock 两个 key),getAPIClient() 是唯一入口。installLangGraphInterceptor(src/core/api/api-client.ts:24-59)在 SDK 上打补丁:threads.create 默认补 ifExists: "do_nothing" 防 409,runs.stream 前先 ensureThread 兜住 SDK 内部 auto-create。注释明确写 owner_sub 注入已移到服务端 acdm_auth,客户端不再传——这是 Aegra 迁移后的契约变更。这块属于 deer-flow 原生流式机制(详见第 31 章),本章只定位它在分层中的位置:core/apicore/threads,再喂 components/workspace/chats

Data Flow:一次业务请求怎么走

关键在第 3-5 步:前端绝不直连 127.0.0.1:8002,而是请求同域 /api/acdm/*,由 next.config.js:110-119 的 rewrite 转发到 ACDM_BACKEND_BASE_URL(dev 默认 http://127.0.0.1:8003,prod 由 Caddy 接管)。同域是 HttpOnly cookie 能用的前提——跨域 fetch 浏览器不会自动带 cookie。生产环境 rewrite 部分被跳过(NEXT_PUBLIC_*_BASE_URL 已设),改由 Caddy 处理(详见第 33 章)。

Implementation Details:basePath 是编译期静态替换

这是全章最重要、也最容易在生产踩坑的一点。

next.config.js:35 一行:

js
// 摘自 deer-flow/frontend/next.config.js:33-35
// 本地 dev 不设 → 走 /;prod build 时 NEXT_PUBLIC_BASE_PATH=/acdm
basePath: process.env.NEXT_PUBLIC_BASE_PATH || undefined,

dev 不设 → basePath 为空,访问 http://localhost:3000/workspace/projects。prod 在 deploy/prod/acdm-frontend/Dockerfile 的 builder stage ENV NEXT_PUBLIC_BASE_PATH=/a-cdmRUN pnpm build——关键就在这个顺序:basePath 是 Next.js 的 build-time 静态替换,pnpm build 时所有 <Link href>next/image src/_next/static/* chunk 路径都被字面量烤成带 /a-cdm 前缀的绝对 URL,写死进 .next 产物。

后果(运维铁律):

  • ❌ 不能在运行时靠 Caddy rewrite / Location header / 环境变量热切 basePath——bundle 里已是死字符串
  • ✅ 改 basePath 必须 docker compose build(prod Dockerfile 还用 ARG BUILD_HASH=$commit_sha 防 BuildKit COPY layer cache 误命中,见 Dockerfile 注释)
  • ✅ 生产 Caddy 只 reverse_proxy acdm-frontend:3000,不感知 basePath——前端 bundle 自己把所有路径带 /a-cdm

prod Dockerfile 还配套设了 NEXT_PRODUCTION_STANDALONE=1,触发 next.config.js:27output: "standalone",产出可独立跑的 server.js(deploy/prod/acdm-frontend/Dockerfile)。compose 里特意取消了 healthcheck——因为 basePath 让 / 必返 404,健康检查会永远 unhealthy 误导排错(deploy/prod/acdm-frontend/compose.yml)。

Configuration

Configdev 默认prod 值含义Source
NEXT_PUBLIC_BASE_PATH不设(空)/a-cdm路径前缀,build 时静态烤入deer-flow/frontend/next.config.js:35
NEXT_PRODUCTION_STANDALONE不设1触发 standalone 产物deer-flow/frontend/next.config.js:27
experimental.proxyTimeout180000msrewrite 反代超时(豆包多模态需 40-100s)deer-flow/frontend/next.config.js:30-32
ACDM_BACKEND_BASE_URLhttp://127.0.0.1:8003Caddy 接管/api/acdm/* rewrite 目标deer-flow/frontend/next.config.js:14-19
NEXT_PUBLIC_BACKEND_BASE_URL空(走 rewrite)/api/deerflow设了则跳过 dev rewritedeer-flow/frontend/src/env.js:28
NEXT_PUBLIC_LANGGRAPH_BASE_URL/api/langgraphLangGraph SDK basedeer-flow/frontend/src/core/config/index.ts:21-40
BETTER_AUTH_SECRET可选必填(32B hex)prod 下 T3 env 强校验,缺则容器 500deer-flow/frontend/src/env.js:10-13
SKIP_ENV_VALIDATION1(Docker build)跳过 env.js zod 校验deer-flow/frontend/src/env.js:54

安全相关:BETTER_AUTH_SECRETNODE_ENV=production 下被 env.js:10-13 标为 z.string() 必填,dev 下 optional。prod compose 注释明确警告:不设则 T3 env 校验失败 → 容器启动 HTTP 500。SKIP_ENV_VALIDATION=1 是 Docker build 阶段的逃生口(runtime 变量由容器注入,build 时还没有)。

Common Pitfalls / 实战 Tips

  • 改 basePath 没重 build = 白改:basePath 是 build-time 替换,只改 env 不 rebuild 完全无效(next.config.js:33-35 注释 + prod Dockerfile)。这是 lessons-learned L26 的来源。
  • /acdm/ vs /a-cdm/ 一字之差:历史上 /acdm/(无连字符)是已删的老 SSG 基座,当前是 /a-cdm/(有连字符)。引用路径时别漏连字符。
  • 别手改 components/ui/**ai-elements/**:它们是 registry 生成物,ESLint 已 ignore(eslint.config.js:10-13),手改会在下次从 registry 重拉时丢失。
  • fetch 必须 credentials: "include" + 同域:core/acdm/api.ts:73 注释强调跨域 cookie 带不回,所有 a-cdm API 走 /api/acdm 相对路径而非绝对 URL。
  • 带 body 的 PATCH/POST 不加 Content-Type 会 422:api.ts:519-527 有 hotfix 注释——FastAPI 把无 Content-Type 的 body 当 plain text,Pydantic 报 422,request() / fetchNoEnvelope() 都已自动补。
  • prod 健康检查必 unhealthy:basePath 让 / 返 404,compose 已主动取消 healthcheck,别被 unhealthy 状态误导(deploy/prod/acdm-frontend/compose.yml)。

References

  • deer-flow/frontend/package.json:1-137 — 技术栈真源(Next 16 / React 19 / Tailwind 4 / pnpm 10.26.2 + 依赖全集)
  • deer-flow/frontend/next.config.js:1-126 — basePath / rewrites / standalone / proxyTimeout 配置
  • deer-flow/frontend/src/env.js:1-60 — T3 env + zod 校验(BETTER_AUTH_SECRET prod 必填)
  • deer-flow/frontend/src/app/layout.tsx:1-36 — 根布局 + Provider 树 + A-CDM metadata
  • deer-flow/frontend/src/app/workspace/layout.tsx:1-39 — 工作台外壳(QueryClient/Sidebar/CommandPalette)
  • deer-flow/frontend/src/core/acdm/index.ts:1-13 — 定制业务桶导出入口
  • deer-flow/frontend/src/core/acdm/api.ts:62-117 — request() fetch 封装(信封解包/401 跳登录/cookie)
  • deer-flow/frontend/src/core/api/api-client.ts:24-120 — LangGraph SDK 单例 + 拦截器
  • deer-flow/frontend/src/proxy.ts:73-99 — 边缘守卫(Next.js 16 中 middleware→proxy)
  • deer-flow/frontend/src/components/workspace/acdm — a-cdm 定制业务组件目录
  • deer-flow/frontend/eslint.config.js:10-13 — ui/ai-elements 生成物 ignore
  • deploy/prod/acdm-frontend/Dockerfile — prod build 烤 basePath=/a-cdm + standalone
  • deploy/prod/acdm-frontend/compose.yml — prod 容器(取消 healthcheck 原因)
PageRelationship
proxy 鉴权守卫与 API 反代本章 proxy.ts 边缘守卫与 next.config.js rewrites 在该章深挖
项目工作台与工作区本章 /workspace/projects/[id] 三栏布局与业务组件在该章展开
AI 消息流与 SSE 基础设施本章 core/api / core/threads 流式骨架由该章详解
Caddy 网关与生产路由拓扑本章 prod 不走 dev rewrite,改由该章 Caddy 反代;basePath 仅前端感知
CICD 与生产部署运维本章 basePath 编译期约束 → 该章解释"改 basePath 必重 build"运维铁律
鉴权与授权双层体系本章 401 跳 /api/acdm/auth/login 接入该章 Keycloak 链路
系统整体架构本章前端是该章整体拓扑中的 acdm-frontend 节点

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析