AutoGPT Frontend 前端贡献指南:Next.js App Router 客户端优先架构、Orval 类型安全 API 钩子与工程实践
本文基于 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、React18.3.1、TypeScript5.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.x(engines 字段)与 pnpm@10.20.0(packageManager 字段),运行 pnpm dev 前需先安装依赖。
为什么是 Client-first,而不是 Server Components 优先
文档给出了明确的立场:默认使用客户端组件(Default to client components),仅在两种情况下使用 Server Components:
- SEO 需要服务端渲染的 HTML;
- 首字节性能(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 创建新页面
- 在
src/app/(platform)/your-feature/page.tsx创建页面; - 若页面有逻辑,在页面旁创建
usePage.ts钩子; - 子组件放在同级的
components/目录; - 使用生成的 API 钩子获取数据;
- 若页面需要鉴权,确保它位于
(platform)路由组内。
文档给出的示例结构:
app/(platform)/dashboard/
page.tsx
useDashboardPage.ts
components/
StatsPanel/
StatsPanel.tsx
useStatsPanel.ts
从仓库实际结构看,src/app/(platform)/ 下确实组织了 admin、auth、build、copilot、artifacts 等路由组,(platform) 这一带括号的路由组正是 Next.js App Router 中用于鉴权保护页面的常用手段——它不影响 URL 路径,但便于在该层级统一做认证检查。
3.2 更新已有页面中的组件
- 找到页面
src/app/(platform)/your-feature/page.tsx; - 检查其
components/目录; - 若需要改逻辑,看
use[Component].ts钩子; - 若只是渲染问题,看
[Component].tsx文件。
这条路径直接对应下一节"渲染与逻辑分离"的组件结构约定。
3.3 发起新的 API 调用并在 UI 展示
- 确认后端端点已存在于 OpenAPI 规范中;
- 重新生成 API 客户端:
pnpm generate:api; - 通过键入操作名自动导入(auto-import)生成的钩子;
- 在组件或自定义钩子中使用该钩子;
- 处理 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 在设计系统中创建新组件
- 确定原子级别:atom(原子)、molecule(分子)或 organism(有机体);
- 创建目录
src/components/[level]/ComponentName/; - 创建
ComponentName.tsx(仅渲染逻辑); - 若有逻辑,创建
useComponentName.ts; - 为 Storybook 创建
ComponentName.stories.tsx; - 使用 Tailwind + 设计令牌(避免硬编码值);
- 图标只能通过
Icon原子使用 Hugeicons; - 在 Storybook 中测试:
pnpm storybook; - PR 后在 Chromatic 中校验视觉回归。
文档示例结构:
src/components/molecules/DataCard/
DataCard.tsx
DataCard.stories.tsx
useDataCard.ts
仓库中该结构是真实存在的:例如 src/components/molecules/ErrorCard/ 目录下同时包含 ErrorCard.tsx、ErrorCard.stories.tsx、helpers.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/auth、store/store、library/library等"按 tag 组织"的路径下,与文档"按auth、store、library等 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 agents、getV2List store agents、getV2Get 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/notifications→useGetV1GetNotificationPreferencesPOST /api/v2/store/agents→usePostV2CreateStoreAgentDELETE /api/v2/store/submissions/{id}→useDeleteV2DeleteStoreSubmissionGET /api/v2/library/agents→useGetV2ListLibraryAgents
生成的目录 src/app/api/__generated__/endpoints/ 按 API tag 组织(如 auth、store、library),可以手动浏览。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,优先状态就近与直接派生值; - 转换与映射逻辑靠近消费者(钩子),不要放在视图里。
同时有两条明确红线:BackendAPI 与 src/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.json 中 test、test:e2e、test-ui 脚本统一携带该环境变量构建的原因)。
新增标志的步骤:① 把标志加入 Flag 枚举与 FlagValues 类型;② 在 mock 值映射表中提供默认值;③ 在 LaunchDarkly 中配置该标志。
结合源码 src/services/feature-flags/use-get-flag.ts 可以看到该封装远比文档示例更完备,值得补充到实操层面:
Flag枚举当前包含BETA_BLOCKS、MARKETPLACE_SEARCH_TERMS、ENABLE_PLATFORM_PAYMENT、ARTIFACTS、CHAT_MODE_OPTION、BUILDER_CHAT_PANEL、HIRE_EXPERTS、ONBOARDING_BRAIN_DUMP、GRAPHITI_MEMORY、DREAM_PASS_ENABLED等二十余个标志;每个标志在defaultFlags中都有兜底默认值;- 部分标志(如
ONBOARDING_BRAIN_DUMP、GRAPHITI_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_BLOCKS、MARKETPLACE_SEARCH_TERMS、COPILOT_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.ts 与 components/)。文档的补充要求:错误推导/映射放在钩子里完成,组件只接收最终消息,并提供重试操作。
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 加载态规范
- 错误:统一使用设计系统的
ErrorCard(src/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,钩子 camelCase(useXxx),其他文件 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
编写测试的三步:
- 用
@/tests/integrations/test-utils的render()渲染页面; - 用 Orval 生成的 MSW handler(
@/app/api/__generated__/endpoints/{tag}/{tag}.msw.ts)mock API; - 用
screen.findByText、screen.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.tsx、src/mocks/mock-server.ts(同目录还有 mock-handlers.ts、mock-browser.ts),与 orval.config.ts 中 mock.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*.ts与helpers.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 钩子替换
BackendAPI或src/lib/autogpt-server-api/*; - 把表现逻辑移入渲染文件、数据/行为移入钩子;
- 一次性转换留在本地
helpers.ts;可复用逻辑移到src/services/或src/lib/utils.ts。
仓库中 src/components/ 顶层确实保留了 __legacy__ 与 atoms、molecules、organisms、ui、layout、contextual 等并存的目录格局,说明迁移是一个持续进行中的过程——按"触碰即迁移"原则渐进处理,而不是等待一次性重构。
十四、参考与延伸阅读
- 设计系统目录在 Chromatic 上维护(文档给出了具体站点地址,本文不输出外链);
- 环境与 API 客户端的更多示例见 autogpt_platform/frontend/README.md;
- 测试细节完整文档见 autogpt_platform/frontend/TESTING.md;
- 生成管线关键文件:orval.config.ts、scripts/generate-api-queries.ts、src/services/feature-flags/use-get-flag.ts、src/components/atoms/Icon/Icon.tsx、src/lib/react-query/queryClient.ts。
该贡献文档自我定位是"活文档"(living document),欢迎随时以 PR 形式改进。对于 AutoGPT 平台这样"前端 UI + 后端 OpenAPI"双端协作的项目,这份指南的核心价值在于把类型安全的数据流(Orval 生成 → 生成钩子 → 就近缓存失效)与渲染/逻辑分离的组件契约固化为可检查的 PR 门禁,使得多人迭代下前端架构不会向"任意 fetch + 巨型组件"退化。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00