首页
/ AutoGPT Frontend 前端贡献指南:Next.js App Router 客户端优先架构、Orval 类型安全 API 钩子与工程实践

AutoGPT Frontend 前端贡献指南:Next.js App Router 客户端优先架构、Orval 类型安全 API 钩子与工程实践

2026-09-06 12:47:50作者:宗隆裙

本文基于 AutoGPT 平台前端(autogpt_platform/frontend/)的官方贡献文档 CONTRIBUTING.md,系统讲解该项目前端的贡献流程与工程约定:Next.js App Router 下的"客户端优先"架构决策、基于 Orval 从 OpenAPI 规范生成类型安全 React Query 钩子的数据获取模式、组件分层结构与命名规范、LaunchDarkly 特性开关、错误与加载态处理、Zustand 复杂流程状态管理,以及测试、脚本工具链与 PR 检查清单。读完本文,你可以在该仓库中独立完成新页面、新组件、新 API 调用的开发,并通过 pnpm format && pnpm lint && pnpm types 与集成测试的完整校验。

一、技术栈总览:Client-first 的 Next.js 应用

AutoGPT 前端的技术栈可以概括为一句话(文档标题即如此定义):Next.js App Router、Client-first、类型安全生成式 API 钩子、Tailwind + shadcn/ui。结合 package.json 可以确认当前实际依赖的关键版本:

  • Next.js 15.5.21、React 18.3.1、TypeScript 5.9.3
  • 数据层:@tanstack/react-query(React Query v5)+ orval(从 OpenAPI 生成客户端)+ zod
  • 样式与设计系统:tailwindcss + tailwind-merge + class-variance-authority + Radix Primitives(即 shadcn/ui 的底座)+ tailwind-scrollbar
  • 特性开关:launchdarkly-react-client-sdk
  • 复杂流程状态:zustand
  • 图标:@hugeicons/core-free-icons + @hugeicons/react
  • 测试:Vitest + React Testing Library + MSW(集成测试)、Playwright(E2E)、Storybook + Chromatic(设计系统);
  • 错误上报:@sentry/nextjs

工程上还要求 Node 24.xengines 字段)与 pnpm@10.20.0packageManager 字段),运行 pnpm dev 前需先安装依赖。

为什么是 Client-first,而不是 Server Components 优先

文档给出了明确的立场:默认使用客户端组件(Default to client components),仅在两种情况下使用 Server Components:

  1. SEO 需要服务端渲染的 HTML;
  2. 首字节性能(TTFB)有极端优化需求。

文档解释了这一取舍:Server Components/Actions 虽然处于技术前沿,但引入了并非总能被其收益正当化的复杂度层;客户端优先保持了开发者心智模型的简单性,尤其对不熟悉 Next.js 或重度前端开发的贡献者更友好。配套原则包括:

  • 能用 Next.js API 路由时,优先于 Server Actions;
  • 组件保持小而简单:优先组合(composition),把大组件拆成小 UI 片段;
  • 状态尽量就近(state colocation);
  • 渲染逻辑与副作用分离(关注点分离);
  • 不重复造轮子。

当确实要渲染服务端数据时,推荐 服务端预取(server-side prefetch)+ 客户端水合(client hydration) 的 React Query 方案,而非把数据逻辑推入服务端组件树(本文第四节的预取示例即此模式)。

二、贡献流程:从分支到合并

1) 基于 dev 分支开分支

  • 功能与修复均从 dev 分支拉出;
  • PR 保持聚焦,目标是一个 PR 对应一个 ticket
  • 使用带 scope 的 Conventional Commits 提交信息,例如 feat(frontend): add X

2) 特性开关保护跨 PR 的功能

如果一个功能要分多个 PR 逐步上线,就用特性开关(Feature Flag)保护,允许迭代式合并(详见第五节)。原则是避免长生命周期特性分支

3) 提 PR 前的自检清单

文档要求请求评审前完成:

  • [x] 代码符合本文档所述的架构与约定;
  • [x] pnpm format && pnpm lint && pnpm types 全部通过;
  • [x] 相关测试本地通过:pnpm test 及/或 Storybook 测试;
  • [x] 若改动 UI,对照设计系统与 Storybook stories 校验视觉效果。

4) 合并进 dev

  • 使用 squash merge
  • squash 标题遵循 Conventional Commits 格式。

三、快速上手 FAQ:四类高频任务的路线图

文档以"Quick Start FAQ"为新人提供了四条捷径,这里完整继承并结合仓库实际目录结构展开。

3.1 创建新页面

  1. src/app/(platform)/your-feature/page.tsx 创建页面;
  2. 若页面有逻辑,在页面旁创建 usePage.ts 钩子;
  3. 子组件放在同级的 components/ 目录;
  4. 使用生成的 API 钩子获取数据;
  5. 若页面需要鉴权,确保它位于 (platform) 路由组内。

文档给出的示例结构:

app/(platform)/dashboard/
  page.tsx
  useDashboardPage.ts
  components/
    StatsPanel/
      StatsPanel.tsx
      useStatsPanel.ts

从仓库实际结构看,src/app/(platform)/ 下确实组织了 adminauthbuildcopilotartifacts 等路由组,(platform) 这一带括号的路由组正是 Next.js App Router 中用于鉴权保护页面的常用手段——它不影响 URL 路径,但便于在该层级统一做认证检查。

3.2 更新已有页面中的组件

  1. 找到页面 src/app/(platform)/your-feature/page.tsx
  2. 检查其 components/ 目录;
  3. 若需要改逻辑,看 use[Component].ts 钩子;
  4. 若只是渲染问题,看 [Component].tsx 文件。

这条路径直接对应下一节"渲染与逻辑分离"的组件结构约定。

3.3 发起新的 API 调用并在 UI 展示

  1. 确认后端端点已存在于 OpenAPI 规范中;
  2. 重新生成 API 客户端:pnpm generate:api
  3. 通过键入操作名自动导入(auto-import)生成的钩子;
  4. 在组件或自定义钩子中使用该钩子;
  5. 处理 loading、error、success 三种状态。

文档给出的示例:

import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";

export function useAgentList() {
  const { data, isLoading, isError, error } = useGetV2ListLibraryAgents();

  return {
    agents: data?.data || [],
    isLoading,
    isError,
    error,
  };
}

需要说明的是:src/app/api/__generated__/ 目录不在仓库中提交,它由 pnpm generate:api 在本地生成(package.json 中该脚本为 npx --yes tsx ./scripts/generate-api-queries.ts && orval --config ./orval.config.ts)。这也是 pnpm dev 脚本先执行 generate:api:force 再启动 next dev --turbo 的原因——每次开发启动都会强制刷新客户端。

3.4 在设计系统中创建新组件

  1. 确定原子级别:atom(原子)、molecule(分子)或 organism(有机体);
  2. 创建目录 src/components/[level]/ComponentName/
  3. 创建 ComponentName.tsx(仅渲染逻辑);
  4. 若有逻辑,创建 useComponentName.ts
  5. 为 Storybook 创建 ComponentName.stories.tsx
  6. 使用 Tailwind + 设计令牌(避免硬编码值);
  7. 图标只能通过 Icon 原子使用 Hugeicons;
  8. 在 Storybook 中测试:pnpm storybook
  9. PR 后在 Chromatic 中校验视觉回归。

文档示例结构:

src/components/molecules/DataCard/
  DataCard.tsx
  DataCard.stories.tsx
  useDataCard.ts

仓库中该结构是真实存在的:例如 src/components/molecules/ErrorCard/ 目录下同时包含 ErrorCard.tsxErrorCard.stories.tsxhelpers.ts 与本地 components/ 子目录,完全符合"渲染/逻辑/helpers/子组件"四分法。

四、数据获取模式:Orval 生成的类型安全钩子

这是本文档最核心的工程约定:所有 API 钩子都由 Orval 从后端 OpenAPI 规范生成,钩子是类型安全的,命名遵循后端 API 的 operation ID。

4.1 生成管线:openapi.json → transformers → 按 tag 拆分

查看 orval.config.ts 可以看到完整生成管线:

export default defineConfig({
  autogpt_api_client: {
    input: {
      target: `./src/app/api/openapi.json`,
      override: {
        transformer: "./src/app/api/transformers/fix-tags.mjs",
      },
    },
    output: {
      workspace: "./src/app/api",
      target: `./__generated__/endpoints`,
      schemas: "./__generated__/models",
      mode: "tags-split",
      client: "react-query",
      httpClient: "fetch",
      indexFiles: false,
      mock: {
        type: "msw",
        baseUrl: "/api/proxy",
        generateEachHttpStatus: true,
        delay: 0,
      },
      override: {
        mutator: {
          path: "./mutators/custom-mutator.ts",
          name: "customMutator",
        },
        query: {
          useQuery: true,
          useMutation: true,
          usePrefetch: true,
        },
        useDates: true,
        operations: {
          "getV2List library agents": {
            query: { useInfinite: true, useInfiniteQueryParam: "page" },
          },
          // ... 更多操作
        },
      },
    },
    hooks: {
      afterAllFilesWrite: "prettier --ignore-path= --write ./src/app/api/__generated__",
    },
  },
});

几个值得注意的实现细节:

  • mode: "tags-split":按 OpenAPI 的 tag 拆分输出目录,这就是为什么钩子位于 __generated__/endpoints/auth/authstore/storelibrary/library 等"按 tag 组织"的路径下,与文档"按 authstorelibrary 等 API tag 浏览"的说法一致;
  • client: "react-query" + httpClient: "fetch":生成物是 React Query v5 的 useQuery/useMutation/预取函数,底层用 fetch
  • usePrefetch: true:每个查询操作都会生成 prefetch*Query 函数,供服务端预取 + 水合使用(见 4.4);
  • mock 配置:同时生成 MSW(Mock Service Worker)mock handler,且 generateEachHttpStatus: true 会为每个 HTTP 状态码生成独立 handler——这正是文档中测试示例能直接引用 getGetV2ListLibraryAgentsMockHandler200 这种"带状态码后缀"命名的原因;
  • operations 覆盖:对分页列表类操作(如 getV2List library agentsgetV2List store agentsgetV2Get builder blocks 等)显式开启 useInfinite,以 page 作为无限查询参数——这意味着这些操作生成的钩子直接支持 useInfinite 无限滚动用法;
  • mutator:所有请求走自定义 mutator(./mutators/custom-mutator.ts),统一注入请求头/代理行为;
  • useDates: true:自动处理日期类型;
  • afterAllFilesWrite:生成完成后自动跑 Prettier 格式化整个 __generated__ 目录。

另外,配置中保留了一段被注释掉的 autogpt_zod_schema 块(生成 zod schema 客户端),说明 zod schema 生成是该管线规划中的一部分;文档正文也建议"在适用处使用生成客户端中的 Zod schema"。

4.2 如何发现钩子:命名规律

文档给出核心规律:use{Method}{Version}{OperationName},且 IDE 自动导入基于 operation ID 给出建议。示例映射:

  • GET /api/v1/notificationsuseGetV1GetNotificationPreferences
  • POST /api/v2/store/agentsusePostV2CreateStoreAgent
  • DELETE /api/v2/store/submissions/{id}useDeleteV2DeleteStoreSubmission
  • GET /api/v2/library/agentsuseGetV2ListLibraryAgents

生成的目录 src/app/api/__generated__/endpoints/ 按 API tag 组织(如 authstorelibrary),可以手动浏览。OpenAPI 规范来源为后端生产/预发环境的 openapi.json 端点(文档列出了两个地址,本文按规范不输出外链)。

4.3 生成式 Query 与 Mutation 的用法

Query(查询)——文档示例,注意 query.select 用于在钩子层完成数据裁剪:

import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth";

export function PreferencesPanel() {
  const { data, isLoading, isError } = useGetV1GetNotificationPreferences({
    query: {
      select: (res) => res.data,
    },
  });

  if (isLoading) return null;
  if (isError) throw new Error("Failed to load preferences");
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Mutation(变更)——配合 get*QueryKey 辅助函数做缓存失效,这是生成客户端的一大价值:query key 不需要手工维护,Orval 同时生成 key 构造函数:

import { useQueryClient } from "@tanstack/react-query";
import {
  useDeleteV2DeleteStoreSubmission,
  getGetV2ListMySubmissionsQueryKey,
} from "@/app/api/__generated__/endpoints/store/store";

export function DeleteSubmissionButton({ submissionId }: { submissionId: string }) {
  const queryClient = useQueryClient();
  const { mutateAsync: deleteSubmission, isPending } =
    useDeleteV2DeleteStoreSubmission({
      mutation: {
        onSuccess: () => {
          queryClient.invalidateQueries({
            queryKey: getGetV2ListMySubmissionsQueryKey(),
          });
        },
      },
    });

  async function onClick() {
    await deleteSubmission({ submissionId });
  }

  return (
    <button disabled={isPending} onClick={onClick}>
      Delete
    </button>
  );
}

服务端预取 + 客户端水合——这是文档推荐的、在不破坏 client-first 的前提下改善 TTFB 的官方模式。服务端组件中使用 getQueryClient(该工厂函数在仓库中位于 src/lib/react-query/queryClient.ts)创建查询客户端,await Promise.all([...prefetch*Query...]) 预取多组数据,再用 HydrationBoundary + dehydrate 注入客户端组件树:

// in a server component
import { getQueryClient } from "@/lib/tanstack-query/getQueryClient";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import {
  prefetchGetV2ListStoreAgentsQuery,
  prefetchGetV2ListStoreCreatorsQuery,
} from "@/app/api/__generated__/endpoints/store/store";

export default async function MarketplacePage() {
  const queryClient = getQueryClient();

  await Promise.all([
    prefetchGetV2ListStoreAgentsQuery(queryClient, { featured: true }),
    prefetchGetV2ListStoreAgentsQuery(queryClient, { sorted_by: "runs" }),
    prefetchGetV2ListStoreCreatorsQuery(queryClient, {
      featured: true,
      sorted_by: "num_agents",
    }),
  ]);

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {/* Client component tree goes here */}
    </HydrationBoundary>
  );
}

注意:文档示例中导入路径写的是 @/lib/tanstack-query/getQueryClient,而仓库中当前实现位于 src/lib/react-query/queryClient.ts(导出 getQueryClient)——从源码结构看该模块经历过目录改名,实际编码时应以仓库当前路径为准。

4.4 状态管理与数据层的硬性红线

文档对状态管理的约定同样关键:

  • 服务端状态优先用 React Query,且就近放置在消费者附近;
  • UI 状态就地放在组件/钩子内,全局状态保持最少;
  • 避免 useMemo/useCallback,除非有实测性能问题;
  • 不滥用 useEffect,优先状态就近与直接派生值;
  • 转换与映射逻辑靠近消费者(钩子),不要放在视图里。

同时有两条明确红线:BackendAPIsrc/lib/autogpt-server-api/* 已被标记为弃用,不得引入新用法;新功能一律使用生成钩子。

五、特性开关:LaunchDarkly 的工程化封装

特性开关由 LaunchDarkly 驱动,统一通过 src/services/feature-flags 下的辅助 API 使用。文档给出两种用法:

客户端组件中检查标志

import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";

export function AgentActivityPanel() {
  const enabled = useGetFlag(Flag.AGENT_ACTIVITY);
  if (!enabled) return null;
  return <div>Feature is enabled!</div>;
}

保护整个路由/页面组件

import { withFeatureFlag } from "@/services/feature-flags/with-feature-flag";

export const MyFeaturePage = withFeatureFlag(function Page() {
  return <div>My feature page</div>;
}, "my-feature-flag");

本地开发与 Playwright:设置 NEXT_PUBLIC_PW_TEST=true 使用 mock 标志值(这也是 package.jsontesttest:e2etest-ui 脚本统一携带该环境变量构建的原因)。

新增标志的步骤:① 把标志加入 Flag 枚举与 FlagValues 类型;② 在 mock 值映射表中提供默认值;③ 在 LaunchDarkly 中配置该标志。

结合源码 src/services/feature-flags/use-get-flag.ts 可以看到该封装远比文档示例更完备,值得补充到实操层面:

  • Flag 枚举当前包含 BETA_BLOCKSMARKETPLACE_SEARCH_TERMSENABLE_PLATFORM_PAYMENTARTIFACTSCHAT_MODE_OPTIONBUILDER_CHAT_PANELHIRE_EXPERTSONBOARDING_BRAIN_DUMPGRAPHITI_MEMORYDREAM_PASS_ENABLED 等二十余个标志;每个标志在 defaultFlags 中都有兜底默认值;
  • 部分标志(如 ONBOARDING_BRAIN_DUMPGRAPHITI_MEMORY)在源码注释中说明镜像后端 Flag 枚举backend/util/feature_flag.py),"关闭时端点 404,两侧必须一致",且刻意默认 false 以实现 fail-closed:LaunchDarkly 宕机或标志缺失时绝不能把功能打开;
  • 支持 NEXT_PUBLIC_FORCE_FLAG_<NAME> 环境变量逐标志覆盖 LaunchDarkly(NAME 为标志值去掉 -、转大写)。源码注释特别解释了实现约束:NEXT_PUBLIC_* 变量在构建期内联进 bundle,所以改后必须重新构建前端镜像;且每个标志必须用字面量 process.env.NEXT_PUBLIC_FORCE_FLAG_X 查找,动态 process.env[envName] 会编译为浏览器侧永远为空的对象读取,导致覆盖静默失效;
  • 数组类型标志(BETA_BLOCKSMARKETPLACE_SEARCH_TERMSCOPILOT_BOT_PLATFORMS)被显式排除在布尔 env 覆盖之外,只走 LaunchDarkly + 默认值回退;
  • useGetFlag 外还有 useFlagStatus,额外返回 ready 状态并带 5 秒超时——文档式提示是:对整条路由做标志门控时,应先判断 ready,避免 LaunchDarkly 尚未响应就短路 notFound() 而误伤真正开启该标志的用户。

六、组件结构:渲染与逻辑强制分离

文档对组件的总原则:渲染逻辑与数据/行为分离,实现细节保持局部化。页面就是由更小组件构成的大组件;子组件在复杂功能中可以继续嵌套。

6.1 基本结构与页面嵌套示例

当组件有非平凡逻辑时,采用四件套结构:

FeatureX/
  FeatureX.tsx        (render logic only)
  useFeatureX.ts      (hook; data fetching, behavior, state)
  helpers.ts          (pure helpers used by the hook)
  components/         (optional, subcomponents local to FeatureX)

文档给出的"页面 + 嵌套子组件"完整示例:

// Page composition
app/(platform)/dashboard/
  page.tsx
  useDashboardPage.ts
    components/ # (Sub-components the dashboard page is made of)
      StatsPanel/
        StatsPanel.tsx
        useStatsPanel.ts
        helpers.ts
        components/ # (Sub-components belonging to StatsPanel)
          StatCard/
            StatCard.tsx
      ActivityFeed/
        ActivityFeed.tsx
        useActivityFeed.ts

6.2 组件编写准则

  • 组件与处理器优先使用函数声明(function declarations);
  • 箭头函数只用于小的内联 lambda(如 map 回调);
  • 避免 barrel 文件与 index.ts 再导出
  • 组件文件保持聚焦可读,复杂逻辑推入 helpers.ts
  • 可复用的跨特性逻辑抽到 src/services/src/lib/utils.ts
  • 组件封装良好,便于在别处复用与抽象;
  • 局部子组件放入父特性的 components/ 目录内。

6.3 简化结构的两种例外

小钩子逻辑(3-4 行)内联到渲染函数

export function ActivityAlert() {
  const [isVisible, setIsVisible] = useState(true);
  if (!isVisible) return null;

  return (
    <Alert onClose={() => setIsVisible(false)}>New activity detected</Alert>
  );
}

纯渲染组件:无钩子逻辑的组件可以不做目录,直接以单文件放 components/ 下:

components/
  ActivityAlert.tsx      (render-only, no folder needed)
  StatsPanel/            (has hook logic, needs folder)
    StatsPanel.tsx
    useStatsPanel.ts

6.4 钩子文件的三条规则

文档示例:

// useStatsPanel.ts
export function useStatsPanel() {
  const [data, setData] = useState<Stats[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchStats().then(setData);
  }, []);

  return {
    data,
    isLoading,
    refresh: () => fetchStats().then(setData),
  };
}

规则:

  • 总是返回一个对象,向视图暴露数据与方法;
  • 只导出一个与组件同名的函数StatsPanel.tsx 对应 useStatsPanel);
  • 钩子逻辑变大时抽入 helpers.ts,让钩子文件"扫一眼可读",不必陷入实现细节。

七、错误处理与加载态:三层策略 + 全局兜底

7.1 渲染/运行时错误:ErrorCard

import { ErrorCard } from "@/components/molecules/ErrorCard";

export function DataPanel() {
  const { data, isLoading, isError, error } = useGetData();

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorCard error={error} />;

  return <div>{data.content}</div>;
}

该组件在仓库中真实存在:src/components/molecules/ErrorCard/ErrorCard.tsx,并遵循前述四件套结构(同目录有 helpers.tscomponents/)。文档的补充要求:错误推导/映射放在钩子里完成,组件只接收最终消息,并提供重试操作。

7.2 API 变更(mutation)错误:Toast 提示

import { useToast } from "@/components/ui/use-toast";

export function useUpdateSettings() {
  const { toast } = useToast();
  const { mutateAsync: updateSettings } = useUpdateSettingsMutation({
    mutation: {
      onError: (error) => {
        toast({
          title: "Failed to update settings",
          description: error.message,
          variant: "destructive",
        });
      },
    },
  });

  return { updateSettings };
}

7.3 手动上报 Sentry

import * as Sentry from "@sentry/nextjs";

try {
  await riskyOperation();
} catch (error) {
  Sentry.captureException(error, {
    tags: { context: "feature-x" },
    extra: { metadata: additionalData },
  });
  throw error;
}

7.4 全局错误边界

应用已配置错误边界:全局捕获未处理异常并上报 Sentry、展示用户友好的错误 UI、防止整个应用崩溃。除非需要自定义错误恢复逻辑,不需要手动给组件包错误边界

7.5 加载态规范

  • 错误:统一使用设计系统的 ErrorCardsrc/components/molecules/ErrorCard/ErrorCard.tsx)展示 API/HTTP 错误与重试;
  • 加载:使用设计系统的 Skeleton 组件族,优先采用与领域匹配的骨架布局(列表、卡片、表格)而非转圈 spinner;
  • 模式参考 Storybook 中 Atoms/Skeleton 的示例。

八、样式、图标与响应式

样式

  • 使用 Tailwind 工具类,偏好语义化、可组合的类名;
  • shadcn/ui 组件作为可用时的基础构件;滚动条样式用 tailwind-scrollbar 插件;
  • 响应式与暗色模式行为与设计系统保持一致。

两条更严格的补充规则(容易被忽略):

  • 不要在特性代码里直接导入 shadcn 原始 primitives——shadcn 只是底层骨架,必须经由 src/components 下的设计系统组件消费;
  • 能用设计令牌就不用 Tailwind 默认主题值(颜色、间距、圆角、字体),存在令牌就不要硬编码默认调色板。

同时,src/components/_legacy__ 下的内容已弃用:新代码禁用,触碰到旧代码时应顺手迁移走。

图标:只允许 Hugeicons,且必须经过 Icon 原子(数据来自 @hugeicons/core-free-icons 的 stroke-rounded 变体)。仓库中的实现 src/components/atoms/Icon/Icon.tsx 证实了文档描述:ICON_STROKE_WIDTH = 2(设计系统 2px 线宽)、size 默认 "1em" 使图标随周围文字缩放,并封装了 createIconComponent 用于需要统一组件类型的图标映射表。直接使用 HugeiconsIcon 是被禁止的:

import { PlusSignIcon } from "@hugeicons/core-free-icons";
import { Icon } from "@/components/atoms/Icon/Icon";

export function CreateButton() {
  return (
    <button type="button" className="inline-flex items-center gap-2">
      <Icon icon={PlusSignIcon} size={16} />
      Create
    </button>
  );
}

当需要给 prop 或配置项声明"携带图标的类型"时使用 IconSvgElement

import type { IconSvgElement } from "@hugeicons/react";

interface Props {
  icon: IconSvgElement;
}

响应式:移动优先,新 UI 必须从 375px 视口宽(iPhone SE)起良好展示;在 375、768、1024、1280 常用断点校验布局;小屏优先堆叠与渐进式披露(progressive disclosure)。

九、复杂流程状态:就近的 Zustand store

对于复杂状态组件、多步向导、跨组件协同的流程,文档推荐小而就近的 Zustand store

  • store 与特性同目录(如 FeatureX/store.ts);
  • 暴露带类型的 selector 以最小化重渲染;
  • 副作用与 API 调用留在钩子里;store 只持有状态与纯动作。

文档给出的 store + selector 示例:

import { create } from "zustand";

interface WizardState {
  step: number;
  data: Record<string, unknown>;
  next(): void;
  back(): void;
  setField(args: { key: string; value: unknown }): void;
}

export const useWizardStore = create<WizardState>((set) => ({
  step: 0,
  data: {},
  next() {
    set((state) => ({ step: state.step + 1 }));
  },
  back() {
    set((state) => ({ step: Math.max(0, state.step - 1) }));
  },
  setField({ key, value }) {
    set((state) => ({ data: { ...state.data, [key]: value } }));
  },
}));

// Usage in a component (selectors keep updates scoped)
function WizardFooter() {
  const step = useWizardStore((s) => s.step);
  const next = useWizardStore((s) => s.next);
  const back = useWizardStore((s) => s.back);

  return (
    <div className="flex items-center gap-2">
      <button onClick={back} disabled={step === 0}>Back</button>
      <button onClick={next}>Next</button>
    </div>
  );
}

以及"钩子 + store 协同的异步动作"示例:

// FeatureX/useFeatureX.ts
import { useMutation } from "@tanstack/react-query";
import { useWizardStore } from "./store";

export function useFeatureX() {
  const setField = useWizardStore((s) => s.setField);
  const next = useWizardStore((s) => s.next);

  const { mutateAsync: save, isPending } = useMutation({
    mutationFn: async (payload: unknown) => {
      // call API here
      return payload;
    },
    onSuccess(data) {
      setField({ key: "result", value: data });
      next();
    },
  });

  return { save, isSaving: isPending };
}

zustand 确实在 package.json 依赖中(5.0.8),与文档一致。

十、命名规范

文档列出的规范汇总:

通用:变量与函数读起来像平实的英语;能用 const 就不用 let;用可搜索的常量代替魔法数字。

文件:组件文件 PascalCase,钩子 camelCaseuseXxx),其他文件 kebab-case;不建 barrel 文件 / index.ts 再导出。

类型

  • 对象形状优先 interface
  • 组件 props 用不导出的 interface Props { ... }
  • 只有当接口需要在组件外使用时才导出具体名称(如 export interface MyComponentProps);
  • 类型定义与组件内联,不单独建 types.ts,除非类型跨多文件共享;
  • 使用精确类型,避免 any 与不安全断言。

文档给出的正误对照:

// ✅ Good - internal props, not exported
interface Props {
  title: string;
  onClose: () => void;
}

export function Modal({ title, onClose }: Props) {
  // ...
}

// ✅ Good - exported when needed externally
export interface ModalProps {
  title: string;
  onClose: () => void;
}

export function Modal({ title, onClose }: ModalProps) {
  // ...
}

// ❌ Bad - unnecessarily specific name for internal use
interface ModalComponentProps {
  title: string;
  onClose: () => void;
}

// ❌ Bad - separate types.ts file for single component
// types.ts
export interface ModalProps { ... }

// Modal.tsx
import type { ModalProps } from './types';

参数:多参数时传单个 Args 对象以增强可读性。

注释:保持最少,只记录非显而易见的意图、不变量与注意事项。

函数与控制流:组件/处理器优先函数声明;箭头函数只用于小型内联回调;用 early return 降低嵌套;不要 catch 了却不处理。

十一、测试体系:以页面级集成测试为默认

文档将完整细节指向 TESTING.md,但核心原则必须在本文覆盖:

集成测试是默认(约占全部测试 90%):在页面级测试——用 React Testing Library 渲染页面、用 MSW(由 Orval 自动生成)mock API 请求、用 testing-library 查询断言:

pnpm test:unit              # run integration/unit tests
pnpm test:unit:watch        # watch mode

测试文件位置:与被测页面/组件同级的 __tests__/ 目录:

app/(platform)/library/
  __tests__/
    main.test.tsx           # main page rendering & interactions
    search.test.tsx         # search-specific behavior
  components/
  page.tsx
  useLibraryPage.ts

编写测试的三步

  1. @/tests/integrations/test-utilsrender() 渲染页面;
  2. 用 Orval 生成的 MSW handler(@/app/api/__generated__/endpoints/{tag}/{tag}.msw.ts)mock API;
  3. screen.findByTextscreen.getByRole 等断言。

文档示例:

import { render, screen } from "@/tests/integrations/test-utils";
import { server } from "@/mocks/mock-server";
import { getGetV2ListLibraryAgentsMockHandler200 } from "@/app/api/__generated__/endpoints/library/library.msw";
import LibraryPage from "../page";

test("renders agent list", async () => {
  server.use(getGetV2ListLibraryAgentsMockHandler200());
  render(<LibraryPage />);
  expect(await screen.findByText("My Agents")).toBeDefined();
});

这些基础设施在仓库中都真实存在:src/tests/integrations/test-utils.tsxsrc/mocks/mock-server.ts(同目录还有 mock-handlers.tsmock-browser.ts),与 orval.config.tsmock.generateEachHttpStatus: true 的配置闭环——每个操作按状态码生成的 handler(如 ...MockHandler200)正是测试里可直接 server.use(...) 的单元。

各类测试的适用场景(文档表格):

类型 适用场景
集成测试(Vitest + RTL + MSW) 所有新页面与新功能的默认选择
E2E(Playwright) 鉴权流程、支付、跨页面导航
Storybook src/components/ 下的设计系统组件

TDD 工作流:① 写失败的测试(集成测试,或带 .fixme 的 Playwright 用例);② 实现修复/功能;③ 移除注解并跑全量套件。

十二、工具链与脚本速查

文档列出的常用脚本(完整清单见 package.json),这里补充实际脚本定义作为佐证:

脚本 作用 实际命令(来自 package.json)
pnpm dev 启动开发服务器(先强制生成 API 客户端) pnpm run generate:api:force && next dev --turbo
pnpm build 生产构建 cross-env NODE_OPTIONS=--max-old-space-size=16384 next build
pnpm start 启动生产服务器 next start
pnpm lint ESLint + Prettier 检查 next lint && prettier --check .
pnpm format 格式化 next lint --fix; prettier --write .
pnpm types 类型检查 tsc --noEmit
pnpm test:unit 集成/单元测试(Vitest + RTL + MSW) vitest run --coverage
pnpm test:unit:watch 集成测试 watch 模式 vitest
pnpm test Playwright E2E NEXT_PUBLIC_PW_TEST=true next build --turbo && pnpm test:e2e:no-build
pnpm storybook 运行 Storybook storybook dev -p 6006
pnpm generate:api 拉取 OpenAPI 规范并重新生成客户端 npx --yes tsx ./scripts/generate-api-queries.ts && orval --config ./orval.config.ts

另有 pnpm gentests(Playwright codegen 录制)、pnpm test-storybook:ci(构建 Storybook 并跑视觉测试)等扩展脚本。值得注意的工程细节:E2E 脚本统一以 NEXT_PUBLIC_PW_TEST=true 构建,与特性开关一节中"Playwright 下使用 mock 标志值"的机制是同一件事;pnpm dev 依赖的 generate:api:force 会带 --force 重新抓取规范,保证本地客户端与后端规范同步。

十三、PR 检查清单与遗留代码迁移指南

PR 检查清单(Frontend)——提交前逐条核对:

  • 客户端优先:Server Components 仅用于 SEO 或极端 TTFB 需求;
  • 使用生成的 API 钩子;无新增 BackendAPI 用法;
  • UI 使用 src/components 原语;不新增 _legacy__ 组件;
  • 非平凡逻辑拆入 use*.tshelpers.ts
  • 可复用逻辑在合适时抽到 src/services/src/lib/utils.ts
  • 导航使用 Next.js router;
  • 新页面/功能新增或更新集成测试(pnpm test:unit);
  • lint、format、type-check 与测试本地全部通过;
  • UI 有变更时更新/新增 Storybook stories 并在 Storybook 中验证。

遗留代码迁移指南——触碰到旧代码时:

  • src/components 下的现代设计系统组件替换 src/components/_legacy__/* 的用法;
  • 用生成的 API 钩子替换 BackendAPIsrc/lib/autogpt-server-api/*
  • 把表现逻辑移入渲染文件、数据/行为移入钩子;
  • 一次性转换留在本地 helpers.ts;可复用逻辑移到 src/services/src/lib/utils.ts

仓库中 src/components/ 顶层确实保留了 __legacy__atomsmoleculesorganismsuilayoutcontextual 等并存的目录格局,说明迁移是一个持续进行中的过程——按"触碰即迁移"原则渐进处理,而不是等待一次性重构。

十四、参考与延伸阅读

该贡献文档自我定位是"活文档"(living document),欢迎随时以 PR 形式改进。对于 AutoGPT 平台这样"前端 UI + 后端 OpenAPI"双端协作的项目,这份指南的核心价值在于把类型安全的数据流(Orval 生成 → 生成钩子 → 就近缓存失效)渲染/逻辑分离的组件契约固化为可检查的 PR 门禁,使得多人迭代下前端架构不会向"任意 fetch + 巨型组件"退化。

登录后查看全文
热门项目推荐
相关项目推荐