基于 OpenClaw + Claude Code 的端到端研发自动化系统
随着 AI Coding 技术的快速发展,OpenClaw 和 Claude Code 等工具已经能够承担从需求分析到代码生成的全流程自动化任务。本规范旨在建立一套标准化的前端代码生成体系,确保 AI 生成的代码质量、可维护性和一致性。
最大化 AI 代码生成覆盖率(目标 80%+),同时通过规范化约束确保代码质量
| 类别 | 技术选型 | 版本要求 | 说明 |
|---|---|---|---|
| 语言 | TypeScript | 5.4+ | 必须使用严格模式 |
| 框架 | React 19 / Vue 3.5 / Angular 18 | 最新稳定版 | 根据项目需求选择 |
| 构建工具 | Vite 6 / Turbopack | 最新版 | 优先使用 Vite |
| 包管理 | pnpm / bun | 最新版 | 推荐 pnpm |
| 代码检查 | ESLint 9 + Biome | 最新版 | Biome 替代 ESLint+Prettier |
| 类型检查 | tsgo (TypeScript Go) | Preview | 性能提升 10 倍 |
| 测试框架 | Vitest + Playwright | 最新版 | 单元测试+E2E 测试 |
使用 tsgo(TypeScript Go 版本)进行类型检查,速度提升约 10 倍,且能捕获更多类型错误
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import type { FC, PropsWithChildren } from 'react';
import styles from './ComponentName.module.css';
// ============ Types ============
export interface ComponentNameProps {
/** 组件标题 */
title: string;
/** 是否显示 */
visible?: boolean;
/** 点击回调 */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** 自定义类名 */
className?: string;
/** 子元素 */
children?: React.ReactNode;
}
// ============ Constants ============
const DEFAULT_VISIBLE = true;
const COMPONENT_DISPLAY_NAME = 'ComponentName';
/**
* ComponentName 组件描述
* @example <ComponentName title="示例标题" visible={true} />
*/
export const ComponentName: FC<ComponentNameProps> = ({
title,
visible = DEFAULT_VISIBLE,
onClick,
className = '',
children,
}) => {
// ============ State ============
const [internalState, setInternalState] = useState<boolean>(false);
// ============ Effects ============
useEffect(() => {
console.log(`${COMPONENT_DISPLAY_NAME} mounted`);
return () => {
console.log(`${COMPONENT_DISPLAY_NAME} unmounted`);
};
}, []);
// ============ Handlers ============
const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
}, [onClick]);
// ============ Memoized Values ============
const processedTitle = useMemo(() => {
return title.trim();
}, [title]);
// ============ Render ============
if (!visible) {
return null;
}
return (
<div className={`${styles.container} ${className}`} data-testid="component-name">
<h2 className={styles.title}>{processedTitle}</h2>
<button
className={styles.button}
onClick={handleClick}
type="button"
aria-label={`Click ${processedTitle}`}
>
{children || 'Click Me'}
</button>
</div>
);
};
ComponentName.displayName = COMPONENT_DISPLAY_NAME;
export default ComponentName;
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import type { Ref } from 'vue';
// ============ Types ============
export interface ComponentNameProps {
title: string;
visible?: boolean;
items?: Array<{ id: number; name: string }>;
}
// ============ Props ============
const props = withDefaults(defineProps<ComponentNameProps>(), {
visible: true,
items: () => [],
});
// ============ Emits ============
const emit = defineEmits<{
click: [event: MouseEvent];
update: [value: string];
}>();
// ============ Reactive State ============
const internalCount: Ref<number> = ref(0);
const isLoading = ref(false);
// ============ Computed ============
const processedTitle = computed(() => props.title.trim());
const hasItems = computed(() => props.items.length > 0);
// ============ Lifecycle Hooks ============
onMounted(() => {
console.log('Component mounted');
initialize();
});
onUnmounted(() => {
console.log('Component unmounted');
cleanup();
});
// ============ Functions ============
function handleClick(event: MouseEvent): void {
internalCount.value++;
emit('click', event);
}
</script>
<template>
<div v-if="visible" class="component-container">
<h2 class="component-title">{{ processedTitle }}</h2>
<button @click="handleClick">Click Me</button>
</div>
</template>
<style module scoped>
.component-container {
padding: 1rem;
border-radius: 8px;
}
</style>
import { useState, useEffect, useCallback, useRef } from 'react';
export interface UseFetchOptions<T> {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
immediate?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
export interface UseFetchReturn<T> {
data: T | null;
loading: boolean;
error: Error | null;
execute: (overrideOptions?: Partial<UseFetchOptions<T>>) => Promise<void>;
reset: () => void;
}
export function useFetch<T>(options: UseFetchOptions<T>): UseFetchReturn<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const mountedRef = useRef<boolean>(false);
useEffect(() => {
mountedRef.current = true;
return () => { mountedRef.current = false; };
}, []);
const execute = useCallback(async () => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setLoading(true);
setError(null);
try {
const response = await fetch(options.url, {
method: options.method,
signal: abortControllerRef.current.signal,
});
const result: T = await response.json();
setData(result);
options.onSuccess?.(result);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
options.onError?.(err);
}
} finally {
setLoading(false);
}
}, [options]);
useEffect(() => {
if (options.immediate) {
execute();
}
}, [options.immediate, execute]);
return { data, loading, error, execute, reset: () => {
setData(null);
setLoading(false);
setError(null);
}};
}
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
export interface ApiResponse<T = unknown> {
code: number;
message: string;
data: T;
timestamp: number;
}
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
const TIMEOUT = 30000;
const apiClient: AxiosInstance = axios.create({
baseURL: BASE_URL,
timeout: TIMEOUT,
headers: { 'Content-Type': 'application/json' },
});
// Request Interceptor
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
config.headers['X-Request-ID'] = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
return config;
});
// Response Interceptor
apiClient.interceptors.response.use(
(response) => {
const { data } = response;
if (data.code !== 200) {
return Promise.reject(new Error(data.message));
}
return data;
},
async (error) => {
const status = error.response?.status;
if (status === 401) {
// Handle unauthorized
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export const api = {
get: <T>(url: string, config?: AxiosRequestConfig) =>
apiClient.get<ApiResponse<T>>(url, config),
post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
apiClient.post<ApiResponse<T>>(url, data, config),
put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
apiClient.put<ApiResponse<T>>(url, data, config),
delete: <T>(url: string, config?: AxiosRequestConfig) =>
apiClient.delete<ApiResponse<T>>(url, config),
};
export default api;
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import ComponentName from './ComponentName';
describe('ComponentName', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render correctly with required props', () => {
render(<ComponentName title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('should not render when visible is false', () => {
render(<ComponentName title="Test" visible={false} />);
expect(screen.queryByTestId('component-name')).not.toBeInTheDocument();
});
it('should call onClick when button is clicked', async () => {
const handleClick = vi.fn();
render(<ComponentName title="Test" onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should have proper ARIA attributes', () => {
render(<ComponentName title="Accessible" />);
expect(screen.getByRole('button'))
.toHaveAttribute('aria-label', 'Click Accessible');
});
});
components/UserProfile.tsx # ✅ 组件文件 PascalCase
hooks/useFetchData.ts # ✅ hook 文件 camelCase
services/userService.ts # ✅ 服务文件 camelCase
types/user.types.ts # ✅ 类型文件 kebab-case
__tests__/UserProfile.test.tsx # ✅ 测试文件 .test.tsx
user-profile.module.css # ✅ 样式文件 kebab-case
components/userProfile.tsx # ❌ 组件文件应 PascalCase
hooks/usefetchdata.ts # ❌ hook 应 camelCase
Services/UserService.ts # ❌ 服务应 camelCase
Types/UserTypes.ts # ❌ 类型文件应 kebab-case
interface 定义对象类型type 定义联合类型、元组、映射类型any,使用 unknown 替代导入顺序: React → 第三方库 → 类型定义 → 样式 → 内部模块 → 相对导入
组件内部顺序: Props → State → Refs → Effects → Handlers → Memoized → Render
"创建一个表单组件"
问题:过于模糊,缺少具体要求
创建一个用户注册表单组件,包含以下字段:
- 用户名(必填,3-20 个字符,只能包含字母数字下划线)
- 邮箱(必填,需要邮箱格式验证)
- 密码(必填,最少 8 位,包含大小写字母和数字)
- 确认密码(必填,必须与密码一致)
- 同意条款(必填复选框)
技术要求:
- 使用 React Hook Form 进行表单管理
- 使用 Zod 进行 schema 验证
- 实时验证(onBlur)
- 提交时显示加载状态
- 成功后显示 Toast 通知
Step 1: "首先,请定义用户注册表单的数据类型和验证 schema"
Step 2: "基于上面的类型定义,创建表单组件的基础结构"
Step 3: "现在添加表单提交、验证和错误处理逻辑"
Step 4: "最后,为这个组件编写完整的单元测试"
project-root/
├── src/
│ ├── assets/ # 资源文件
│ ├── components/ # 通用组件
│ │ ├── ui/ # UI 基础组件
│ │ ├── business/ # 业务组件
│ │ └── layouts/ # 布局组件
│ ├── hooks/ # 自定义 Hooks
│ ├── pages/ # 页面组件
│ ├── services/ # API 服务
│ ├── store/ # 状态管理
│ ├── styles/ # 全局样式
│ ├── types/ # 类型定义
│ ├── utils/ # 工具函数
│ └── constants/ # 常量定义
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── e2e/ # E2E 测试
├── docs/ # 文档
├── .github/workflows/ # CI/CD
└── package.json
ComponentName/
├── index.ts # 导出入口
├── ComponentName.tsx # 组件主文件
├── ComponentName.module.css # 样式文件
├── ComponentName.test.tsx # 测试文件
├── ComponentName.stories.tsx # Storybook 故事
├── types.ts # 类型定义
└── __tests__/ # 额外测试(可选)
每个组件只做一件事,复杂组件拆分为多个子组件
React.memo() 避免不必要的重渲染useCallback 和 useMemo 优化函数和值| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 组件本地状态 | useState / useReducer |
简单状态用 useState,复杂状态用 useReducer |
| 跨组件共享状态 | Zustand / Jotai | 轻量级全局状态 |
| 服务端状态 | TanStack Query / SWR | 数据获取、缓存、同步 |
| 表单状态 | React Hook Form | 高性能表单管理 |
| 复杂应用状态 | Redux Toolkit | 大型应用,需要时间旅行调试 |
src/
├── services/
│ ├── api.ts # Axios 实例和拦截器
│ └── interceptors/ # 拦截器逻辑
├── api/
│ ├── user.api.ts # 用户相关 API
│ ├── auth.api.ts # 认证相关 API
│ └── product.api.ts # 产品相关 API
├── hooks/
│ └── useUser.ts # 数据获取 Hook
└── types/
└── api.types.ts # API 类型定义
staleTime 和 cacheTimeE2E Tests (10%)
关键用户流程Integration Tests (20%)
组件/API 集成Unit Tests (70%)
纯函数/Hooks语句覆盖率: ≥ 90%
分支覆盖率: ≥ 85%
函数覆盖率: ≥ 80%
| 阶段 | AI 自动化 | 人工审核 | 说明 |
|---|---|---|---|
| 需求分析 | ✅ 需求拆解 | ✅ 需求确认 | AI 拆解需求,人工确认准确性 |
| 技术方案 | ✅ 方案草拟 | ✅ 方案评审 | AI 提供方案,人工审核架构合理性 |
| 代码生成 | ✅ 代码编写 | ⚠️ 抽样审查 | AI 生成代码,人工抽查关键逻辑 |
| Code Review | ✅ 初步检查 | ✅ 深度审查 | AI 检查规范,人工审查业务逻辑 |
| 测试 | ✅ 单元测试 | ✅ 集成测试 | AI 写单测,人工写集成和 E2E 测试 |
| 部署 | ✅ 自动部署 | ⚠️ 发布审批 | AI 自动部署,人工审批生产发布 |
通过 .openclaw/config.yml 配置文件,可以实现 AI 自动生成代码、自动运行测试、自动 Code Review,并在关键节点触发人工审核,实现高效的人机协同开发流程。
# 开发
pnpm dev # 启动开发服务器
pnpm lint # 代码检查
pnpm type-check # 类型检查
pnpm test # 运行测试
pnpm test:coverage # 测试覆盖率
# 构建
pnpm build # 生产构建
pnpm preview # 预览构建结果
# Docker
docker build -t frontend .
docker-compose up
# K8s
kubectl apply -f k8s/
kubectl get pods
kubectl logs -f deployment/frontend