主题
前端核心服务层
本章目标:
- 讲清楚
frontend/src/core下各服务模块(api/auth/skills/mcp/memory/uploads/settings/models/notification/config)如何统一封装对 Gateway API 的调用,以及为什么需要这一层。- 拆解 React Query(
QueryClientProvider)如何承接服务器状态,服务模块如何按api.ts+hooks.ts+types.ts三件套组织。- 梳理每个服务模块与具体 Gateway 路由的对应关系,以及 CSRF / 鉴权 / baseURL 的共享约定。
TL;DR
core 服务层把"前端组件如何调用后端"这件事收敛成一套统一约定:所有 REST 请求都经 core/api/fetcher.ts 的 fetch 封装(自动带 cookie、自动注入 X-CSRF-Token、401 自动跳登录),baseURL 由 core/config 统一推导,LangGraph 流式请求走 core/api/api-client.ts 的单例 SDK。每个业务域(skills/mcp/memory/uploads/models)是一个独立目录,内部用 api.ts(纯请求)+ hooks.ts(React Query 包装)+ types.ts(类型)三件套,服务器状态由 @tanstack/react-query 管理,本地偏好(settings)走 localStorage,认证态由 AuthProvider Context 提供。
Overview
为什么前端不直接在组件里 fetch('/api/skills'),而要先过一层 core 服务封装 + React Query?核心原因有四个,都是横切关注点不能散落在每个调用点:
CSRF 与 Cookie 契约必须全局一致。Gateway 的 CSRFMiddleware 对所有状态变更方法(POST/PUT/DELETE/PATCH)强制 Double Submit Cookie 校验,缺失
X-CSRF-Token头直接返回 403;同时跨源 SSR 路由的请求必须带credentials: "include"才能携带 HttpOnly 的access_tokencookie。这两个契约被收敛进core/api/fetcher.ts的fetch,任何用原生fetch()的调用点都会静默失败 frontend/src/core/api/fetcher.ts:39-89。baseURL 推导逻辑随部署形态变化。是否配置
NEXT_PUBLIC_BACKEND_BASE_URL、是否 mock、SSR 还是浏览器,baseURL 都不同,统一由core/config的getBackendBaseURL/getLangGraphBaseURL计算 frontend/src/core/config/index.ts:11-44。服务器状态需要缓存、失效与去重。skills、mcp 配置、models 列表、memory 这些数据是后端真理来源,组件多处订阅、写操作后要回刷。React Query 用
queryKey做缓存与失效,避免每个组件各自轮询 frontend/src/core/skills/hooks.ts:7-31。认证态需要 SSR 注入避免闪烁。
AuthProvider只持有展示用user,初始值由服务端守卫注入,不在客户端持有任何 JWT frontend/src/core/auth/AuthProvider.tsx:36-50。
Architecture
core 服务层与 Gateway 路由(详见第 13 章)一一对应。整体分两条数据通道:
- REST 通道:业务域目录的
api.ts调用core/api/fetcher.ts的fetch,经core/config拼出 baseURL,打到 Gateway 的 FastAPI 路由(全部prefix="/api",见后端 router)。 - 流式通道:LangGraph 会话流不走 REST 封装,而走
core/api/api-client.ts单例 SDK 客户端,通过onRequest钩子注入 CSRF(第 29 章详述消息流)。
服务器状态由 QueryClientProvider(@tanstack/react-query)在 workspace 子树注入,本地偏好(settings)走 useSyncExternalStore + localStorage,与 React Query 互不干扰。
Source 列表:
core/api/fetcher.ts— 统一fetch封装,CSRF + cookie + 401 跳转 frontend/src/core/api/fetcher.ts:56-89core/api/api-client.ts— LangGraph SDK 单例与 CSRF 钩子 frontend/src/core/api/api-client.ts:34-72core/config/index.ts— baseURL 推导 frontend/src/core/config/index.ts:11-44components/query-client-provider.tsx— React Query 根 Provider frontend/src/components/query-client-provider.tsx:8-20core/auth/AuthProvider.tsx— 认证态 Context frontend/src/core/auth/AuthProvider.tsx:44-134core/auth/server.ts— SSR 侧用户拉取 frontend/src/core/auth/server.ts:12-96
Components / Subsystems
api(请求基础设施)
职责:所有 REST 请求的统一入口与 LangGraph SDK 单例。
fetcher.ts导出fetch:对状态变更方法注入X-CSRF-Token(从csrf_tokencookie 读取,不覆盖调用方已设置的同名头),强制credentials: "include",401 时跳/login并抛错 frontend/src/core/api/fetcher.ts:56-89。readCsrfCookie是 SSR 安全的(document未定义时返回null)frontend/src/core/api/fetcher.ts:29-37。api-client.ts用getAPIClient()返回按mock/default缓存的LangGraphClient单例,onRequest钩子injectCsrfHeader每次请求实时读 cookie(处理登录/登出/改密的 cookie 轮转),并对runs.stream/runs.joinStream包一层sanitizeRunStreamOptions过滤不支持的 streamMode frontend/src/core/api/api-client.ts:42-72。feedback.ts提供upsertFeedback/deleteFeedback,对接PUT|DELETE /api/threads/{id}/runs/{runId}/feedbackfrontend/src/core/api/feedback.ts:11-42。
对接 Gateway:feedback 路由 prefix="/api/threads" backend/app/gateway/routers/feedback.py:19。
auth(认证态)
职责:在客户端只持有展示用 user,从不持有 JWT;SSR 侧用 cookie 拉取用户。
AuthProvider.tsx:Context 提供user / isAuthenticated / isLoading / logout / refreshUser,refreshUser调/api/v1/auth/me,logout调POST /api/v1/auth/logout并立即清本地态防闪烁;tab 重新可见时节流(60s)刷新用户 frontend/src/core/auth/AuthProvider.tsx:56-123。server.ts:getServerSideUser()在 SSR 用access_tokencookie 调内部 Gateway/api/v1/auth/me与/api/v1/auth/setup-status,返回标签联合AuthResult(authenticated / needs_setup / system_setup_required / unauthenticated / gateway_unavailable / config_error),5s 超时 frontend/src/core/auth/server.ts:12-96。gateway-config.ts:从DEER_FLOW_INTERNAL_GATEWAY_BASE_URL读内部网关地址(默认http://127.0.0.1:8001),Zod 校验后缓存 frontend/src/core/auth/gateway-config.ts:12-31。types.ts:userSchema、buildLoginUrl、parseAuthError(解包 FastAPI{detail:{code,message}}多种形态)frontend/src/core/auth/types.ts:63-94。
对接 Gateway:/api/v1/auth/*(详见第 14 章鉴权)。
skills(技能管理)
职责:列出 / 启停技能,安装技能。
api.ts:loadSkills(GET/api/skills,取json.skills)、enableSkill(PUT/api/skills/{name}body{enabled})、installSkill(POST/api/skills/install,失败时把detail转成统一{success:false,...})frontend/src/core/skills/api.ts:6-63。hooks.ts:useSkills(queryKey["skills"])、useEnableSkill(mutation 成功后invalidateQueries(["skills"]))frontend/src/core/skills/hooks.ts:7-31。
对接 Gateway:skills 路由 prefix="/api",含 GET/POST /skills、PUT /skills/{name} 等 backend/app/gateway/routers/skills.py:21-103。
mcp(MCP 配置)
职责:读 / 写 MCP 服务器配置,启停单个 server。
api.ts:loadMCPConfig(GET/api/mcp/config)、updateMCPConfig(PUT/api/mcp/config)frontend/src/core/mcp/api.ts:6-20。hooks.ts:useMCPConfig(queryKey["mcpConfig"])、useEnableMCPServer(读当前配置→合并单个 server 的enabled→PUT 全量→失效缓存)frontend/src/core/mcp/hooks.ts:5-44。
对接 Gateway:mcp 路由 prefix="/api" backend/app/gateway/routers/mcp.py:12。
memory(长期记忆)
职责:读取 / 清空 / 导入导出用户记忆,facts 的增删改。
api.ts:loadMemory(GET/api/memory)、clearMemory(DELETE)、deleteMemoryFact(DELETE/api/memory/facts/{id})、exportMemory、importMemory(POST/api/memory/import)、createMemoryFact(POST/api/memory/facts)、updateMemoryFact(PATCH);统一用readMemoryResponse把后端detail(字符串/数组/对象)归一为错误信息 frontend/src/core/memory/api.ts:10-149。hooks.ts:useMemory(queryKey["memory"]),所有写操作 mutation 成功后用queryClient.setQueryData(["memory"], memory)直接以响应体回写缓存(后端返回完整新状态,免回刷)frontend/src/core/memory/hooks.ts:17-84。
对接 Gateway:memory 路由 prefix="/api" backend/app/gateway/routers/memory.py:18。前端还有一个 Next.js route handler app/api/memory/route.ts 做服务端透传代理(用 NEXT_PUBLIC_BACKEND_BASE_URL 默认 http://127.0.0.1:8001)frontend/src/app/api/memory/route.ts:3-31。
uploads(文件上传)
职责:线程级文件上传 / 列出 / 删除。
api.ts:uploadFiles(POSTFormData到/api/threads/{id}/uploads)、listUploadedFiles(GET.../uploads/list)、deleteUploadedFile(DELETE.../uploads/{filename}),失败时读detailfrontend/src/core/uploads/api.ts:44-107。hooks.ts:useUploadFiles/useDeleteUploadedFile成功后失效["uploads","list",threadId];useUploadedFiles依赖enabled: !!threadId;useUploadFilesOnSubmit把上传封装成提交流程里的回调 frontend/src/core/uploads/hooks.ts:19-79。
对接 Gateway:uploads 路由 prefix="/api/threads/{thread_id}/uploads" backend/app/gateway/routers/uploads.py:34(文件上传与文档转换详见第 27 章)。
settings(本地偏好)
职责:浏览器侧用户偏好(通知开关、token 用量显示、线程级模型覆盖),不走后端。
store.ts:用模块级单例 +Set<Listener>实现外部 store,监听storage事件做跨标签页同步,updateThreadSettings在改context.model_name时单独落到deerflow.thread-model.{id}键 frontend/src/core/settings/store.ts:64-150。hooks.ts:useLocalSettings/useThreadSettings用useSyncExternalStore订阅,SSR 回退DEFAULT_LOCAL_SETTINGSfrontend/src/core/settings/hooks.ts:17-59。
对接 Gateway:无(纯 localStorage,DEFAULT_LOCAL_SETTINGS 见 frontend/src/core/settings/local.ts:4-20)。
models / notification
models:loadModels(GET/api/models,对缺字段做兜底models:[]/token_usage:{enabled:false})frontend/src/core/models/api.ts:5-12;useModelsqueryKey["models"]、refetchOnWindowFocus:falsefrontend/src/core/models/hooks.ts:5-18。注意此处用的是全局fetch而非core/api/fetcher封装(GET 无需 CSRF,但不会自动 401 跳转)。models路由prefix="/api"backend/app/gateway/routers/models.py:7。notification:useNotification是纯浏览器 Notification API 封装,受settings.notification.enabled控制,1s 内防抖,不调用任何后端 frontend/src/core/notification/hooks.ts:22-98。
Data Flow
以"用户在设置面板启停一个技能"为例,展示组件 → core hook → React Query → fetch → Gateway → 缓存回写的完整链路。
下图给出 baseURL 与 CSRF 在 REST / 流式两条通道的分流决策:
速查表
core 服务模块 × 对应 Gateway 路由矩阵(Gateway 路由的 prefix 在后端 router,前端经 getBackendBaseURL() 拼接):
| core 模块 | 关键 hook / 函数 | HTTP 方法 + 路径 | React Query queryKey | Source |
|---|---|---|---|---|
| api/feedback | upsertFeedback / deleteFeedback | PUT/DELETE /api/threads/{id}/runs/{runId}/feedback | —(直接调用) | 前端 / 后端 |
| api/api-client | getAPIClient() | LangGraph SDK → /api/langgraph/* | —(SDK 内部) | 前端 |
| auth | useAuth / getServerSideUser | GET /api/v1/auth/me,POST /api/v1/auth/logout | —(Context) | 前端 / 后端 |
| skills | useSkills / useEnableSkill | GET/POST /api/skills,PUT /api/skills/{name} | ["skills"] | 前端 / 后端 |
| mcp | useMCPConfig / useEnableMCPServer | GET/PUT /api/mcp/config | ["mcpConfig"] | 前端 / 后端 |
| memory | useMemory / useCreateMemoryFact 等 | GET/DELETE /api/memory,POST/PATCH /api/memory/facts | ["memory"] | 前端 / 后端 |
| uploads | useUploadFiles / useUploadedFiles | POST/GET/DELETE /api/threads/{id}/uploads* | ["uploads","list",threadId] | 前端 / 后端 |
| models | useModels | GET /api/models | ["models"] | 前端 / 后端 |
| settings | useLocalSettings / useThreadSettings | —(localStorage) | —(useSyncExternalStore) | 前端 |
| notification | useNotification | —(浏览器 Notification API) | — | 前端 |
| config | getBackendBaseURL / getLangGraphBaseURL | —(baseURL 推导) | — | 前端 |
Common Pitfalls / Tips
- 状态变更必须用
core/api/fetcher.ts的fetch。原生globalThis.fetch()不会注入X-CSRF-Token,Gateway 会返回 403;models/api.ts用的是全局fetch(GET 无 CSRF 要求,但也因此不会自动 401 跳登录)frontend/src/core/models/api.ts:6。 - CSRF 头不覆盖调用方。
fetch仅在调用方没设置X-CSRF-Token时才注入,显式 override 优先 frontend/src/core/api/fetcher.ts:70-72。 - memory 写操作不需要手动失效缓存。后端每次返回完整新状态,hooks 用
setQueryData(["memory"], memory)直接回写;而 skills/mcp/uploads 用invalidateQueries触发回刷,模式不同别混用 frontend/src/core/memory/hooks.ts:25-34。 QueryClient是模块级单例且无自定义默认项(无 staleTime 配置),只在 workspace 子树注入,landing 页不在 Provider 内,不能在那里用 React Query hooks frontend/src/components/query-client-provider.tsx:8 / frontend/src/app/workspace/workspace-content.tsx:26-33。- settings 不是 React Query。它走
useSyncExternalStore+ localStorage,支持跨标签页storage事件同步,SSR 时返回默认值;别试图用 queryKey 去失效它 frontend/src/core/settings/store.ts:93-116。 - baseURL 在未配
NEXT_PUBLIC_BACKEND_BASE_URL时是空串,此时fetch(\${baseURL}/api/skills`)` 退化为同源相对路径,依赖 nginx/Next 代理转发 frontend/src/core/config/index.ts:11-19。 - SSR 安全:
readCsrfCookie与 settings store 都对typeof window/document === "undefined"做了守卫,服务端组件可安全 import,但拿不到真实值 frontend/src/core/api/fetcher.ts:30。
References
- frontend/src/core/api/fetcher.ts — 统一 fetch / CSRF / 401 契约
- frontend/src/core/api/api-client.ts — LangGraph SDK 单例与 CSRF 钩子
- frontend/src/core/config/index.ts — baseURL 推导
- frontend/src/components/query-client-provider.tsx — React Query 根 Provider
- frontend/src/core/auth/AuthProvider.tsx — 认证态 Context
- frontend/src/core/auth/server.ts — SSR 用户拉取
- frontend/src/core/skills/hooks.ts — skills 三件套 hooks 范式
- frontend/src/core/memory/hooks.ts — setQueryData 回写范式
- frontend/src/core/settings/store.ts — useSyncExternalStore 本地偏好
Related Pages
| 章节 | 关系 |
|---|---|
| ./13-Gateway-API与路由体系.md | 本章服务层对接的后端路由全貌,prefix 与 router 定义在此 |
| ./14-鉴权-CSRF与授权.md | fetcher.ts 的 CSRF Double Submit 与 401 跳转对应的后端鉴权机制 |
| ./28-前端技术栈与应用结构.md | core 在 src/ 中的定位、env 校验与整体应用结构背景 |
| ./29-AI消息流与流式渲染.md | api-client.ts 单例提供的 LangGraph 流式通道在此详述(本章不重复) |
| ./30-工作区与聊天界面.md | QueryClientProvider 注入点 workspace 子树、threads/todos/artifacts 由该章覆盖 |