diff --git a/docs/zh-CN/skills/csharp-testing/SKILL.md b/docs/zh-CN/skills/csharp-testing/SKILL.md new file mode 100644 index 00000000..f33c48ee --- /dev/null +++ b/docs/zh-CN/skills/csharp-testing/SKILL.md @@ -0,0 +1,321 @@ +--- +name: csharp-testing +description: 使用 xUnit、FluentAssertions、模拟、集成测试和测试组织最佳实践的 C# 和 .NET 测试模式。 +origin: ECC +--- + +# C# 测试模式 + +使用 xUnit、FluentAssertions 和现代测试实践为 .NET 应用程序提供的全面测试模式。 + +## 何时使用 + +* 为 C# 代码编写新测试 +* 审查测试质量和覆盖率 +* 为 .NET 项目搭建测试基础设施 +* 调试不稳定或缓慢的测试 + +## 测试框架栈 + +| 工具 | 用途 | +|---|---| +| **xUnit** | 测试框架(.NET 首选) | +| **FluentAssertions** | 可读的断言语法 | +| **NSubstitute** 或 **Moq** | 模拟依赖项 | +| **Testcontainers** | 集成测试中的真实基础设施 | +| **WebApplicationFactory** | ASP.NET Core 集成测试 | +| **Bogus** | 生成逼真的测试数据 | + +## 单元测试结构 + +### 安排-操作-断言 + +```csharp +public sealed class OrderServiceTests +{ + private readonly IOrderRepository _repository = Substitute.For(); + private readonly ILogger _logger = Substitute.For>(); + private readonly OrderService _sut; + + public OrderServiceTests() + { + _sut = new OrderService(_repository, _logger); + } + + [Fact] + public async Task PlaceOrderAsync_ReturnsSuccess_WhenRequestIsValid() + { + // Arrange + var request = new CreateOrderRequest + { + CustomerId = "cust-123", + Items = [new OrderItem("SKU-001", 2, 29.99m)] + }; + + // Act + var result = await _sut.PlaceOrderAsync(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.CustomerId.Should().Be("cust-123"); + } + + [Fact] + public async Task PlaceOrderAsync_ReturnsFailure_WhenNoItems() + { + // Arrange + var request = new CreateOrderRequest + { + CustomerId = "cust-123", + Items = [] + }; + + // Act + var result = await _sut.PlaceOrderAsync(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Contain("at least one item"); + } +} +``` + +### 使用 Theory 的参数化测试 + +```csharp +[Theory] +[InlineData("", false)] +[InlineData("a", false)] +[InlineData("ab@c.d", false)] +[InlineData("user@example.com", true)] +[InlineData("user+tag@example.co.uk", true)] +public void IsValidEmail_ReturnsExpected(string email, bool expected) +{ + EmailValidator.IsValid(email).Should().Be(expected); +} + +[Theory] +[MemberData(nameof(InvalidOrderCases))] +public async Task PlaceOrderAsync_RejectsInvalidOrders(CreateOrderRequest request, string expectedError) +{ + var result = await _sut.PlaceOrderAsync(request, CancellationToken.None); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Contain(expectedError); +} + +public static TheoryData InvalidOrderCases => new() +{ + { new() { CustomerId = "", Items = [ValidItem()] }, "CustomerId" }, + { new() { CustomerId = "c1", Items = [] }, "at least one item" }, + { new() { CustomerId = "c1", Items = [new("", 1, 10m)] }, "SKU" }, +}; +``` + +## 使用 NSubstitute 进行模拟 + +```csharp +[Fact] +public async Task GetOrderAsync_ReturnsNull_WhenNotFound() +{ + // Arrange + var orderId = Guid.NewGuid(); + _repository.FindByIdAsync(orderId, Arg.Any()) + .Returns((Order?)null); + + // Act + var result = await _sut.GetOrderAsync(orderId, CancellationToken.None); + + // Assert + result.Should().BeNull(); +} + +[Fact] +public async Task PlaceOrderAsync_PersistsOrder() +{ + // Arrange + var request = ValidOrderRequest(); + + // Act + await _sut.PlaceOrderAsync(request, CancellationToken.None); + + // Assert — verify the repository was called + await _repository.Received(1).AddAsync( + Arg.Is(o => o.CustomerId == request.CustomerId), + Arg.Any()); +} +``` + +## ASP.NET Core 集成测试 + +### WebApplicationFactory 设置 + +```csharp +public sealed class OrderApiTests : IClassFixture> +{ + private readonly HttpClient _client; + + public OrderApiTests(WebApplicationFactory factory) + { + _client = factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Replace real DB with in-memory for tests + services.RemoveAll>(); + services.AddDbContext(options => + options.UseInMemoryDatabase("TestDb")); + }); + }).CreateClient(); + } + + [Fact] + public async Task GetOrder_Returns404_WhenNotFound() + { + var response = await _client.GetAsync($"/api/orders/{Guid.NewGuid()}"); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task CreateOrder_Returns201_WithValidRequest() + { + var request = new CreateOrderRequest + { + CustomerId = "cust-1", + Items = [new("SKU-001", 1, 19.99m)] + }; + + var response = await _client.PostAsJsonAsync("/api/orders", request); + + response.StatusCode.Should().Be(HttpStatusCode.Created); + response.Headers.Location.Should().NotBeNull(); + } +} +``` + +### 使用 Testcontainers 进行测试 + +```csharp +public sealed class PostgresOrderRepositoryTests : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder() + .WithImage("postgres:16-alpine") + .Build(); + + private AppDbContext _db = null!; + + public async Task InitializeAsync() + { + await _postgres.StartAsync(); + var options = new DbContextOptionsBuilder() + .UseNpgsql(_postgres.GetConnectionString()) + .Options; + _db = new AppDbContext(options); + await _db.Database.MigrateAsync(); + } + + public async Task DisposeAsync() + { + await _db.DisposeAsync(); + await _postgres.DisposeAsync(); + } + + [Fact] + public async Task AddAsync_PersistsOrder() + { + var repo = new SqlOrderRepository(_db); + var order = Order.Create("cust-1", [new OrderItem("SKU-001", 2, 10m)]); + + await repo.AddAsync(order, CancellationToken.None); + + var found = await repo.FindByIdAsync(order.Id, CancellationToken.None); + found.Should().NotBeNull(); + found!.Items.Should().HaveCount(1); + } +} +``` + +## 测试组织 + +``` +tests/ + MyApp.UnitTests/ + Services/ + OrderServiceTests.cs + PaymentServiceTests.cs + Validators/ + EmailValidatorTests.cs + MyApp.IntegrationTests/ + Api/ + OrderApiTests.cs + Repositories/ + OrderRepositoryTests.cs + MyApp.TestHelpers/ + Builders/ + OrderBuilder.cs + Fixtures/ + DatabaseFixture.cs +``` + +## 测试数据构建器 + +```csharp +public sealed class OrderBuilder +{ + private string _customerId = "cust-default"; + private readonly List _items = [new("SKU-001", 1, 10m)]; + + public OrderBuilder WithCustomer(string customerId) + { + _customerId = customerId; + return this; + } + + public OrderBuilder WithItem(string sku, int quantity, decimal price) + { + _items.Add(new OrderItem(sku, quantity, price)); + return this; + } + + public Order Build() => Order.Create(_customerId, _items); +} + +// Usage in tests +var order = new OrderBuilder() + .WithCustomer("cust-vip") + .WithItem("SKU-PREMIUM", 3, 99.99m) + .Build(); +``` + +## 常见反模式 + +| 反模式 | 修复方法 | +|---|---| +| 测试实现细节 | 测试行为和结果 | +| 共享的可变测试状态 | 每个测试使用新实例(xUnit 通过构造函数实现) | +| 在异步测试中使用 `Thread.Sleep` | 使用带超时的 `Task.Delay` 或轮询辅助方法 | +| 对 `ToString()` 输出进行断言 | 对类型化属性进行断言 | +| 每个测试一个巨型断言 | 每个测试一个逻辑断言 | +| 测试名称描述实现 | 按行为命名:`Method_ExpectedResult_WhenCondition` | +| 忽略 `CancellationToken` | 始终传递并验证取消 | + +## 运行测试 + +```bash +# Run all tests +dotnet test + +# Run with coverage +dotnet test --collect:"XPlat Code Coverage" + +# Run specific project +dotnet test tests/MyApp.UnitTests/ + +# Filter by test name +dotnet test --filter "FullyQualifiedName~OrderService" + +# Watch mode during development +dotnet watch test --project tests/MyApp.UnitTests/ +``` diff --git a/docs/zh-CN/skills/dart-flutter-patterns/SKILL.md b/docs/zh-CN/skills/dart-flutter-patterns/SKILL.md new file mode 100644 index 00000000..c3447178 --- /dev/null +++ b/docs/zh-CN/skills/dart-flutter-patterns/SKILL.md @@ -0,0 +1,565 @@ +--- +name: dart-flutter-patterns +description: 生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。 +origin: ECC +--- + +# Dart/Flutter 模式 + +## 使用场景 + +在以下情况使用此技能: + +* 开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式 +* 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导 +* 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择 +* 实现安全的 HTTP 客户端、WebView 集成或本地存储 +* 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试 +* 使用认证守卫配置 GoRouter + +## 工作原理 + +此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式: + +1. **空安全** — 避免 `!`,优先使用 `?.`/`??`/模式匹配 +2. **不可变状态** — 密封类、`freezed`、`copyWith` +3. **异步组合** — 并发 `Future.wait`、`BuildContext` 后安全使用 `await` +4. **组件架构** — 提取为类(而非方法)、`const` 传播、作用域重建 +5. **状态管理** — BLoC/Cubit 事件、Riverpod 通知器和派生提供者 +6. **导航** — 通过 `refreshListenable` 实现带响应式认证守卫的 GoRouter +7. **网络请求** — 带拦截器的 Dio、带一次性重试守卫的令牌刷新 +8. **错误处理** — 全局捕获、`ErrorWidget.builder`、Crashlytics 集成 +9. **测试** — 单元测试(BLoC 测试)、组件测试(ProviderScope 覆盖)、使用假对象而非模拟对象 + +## 示例 + +```dart +// Sealed state — prevents impossible states +sealed class AsyncState {} +final class Loading extends AsyncState {} +final class Success extends AsyncState { final T data; const Success(this.data); } +final class Failure extends AsyncState { final Object error; const Failure(this.error); } + +// GoRouter with reactive auth redirect +final router = GoRouter( + refreshListenable: GoRouterRefreshStream(authCubit.stream), + redirect: (context, state) { + final authed = context.read().state is AuthAuthenticated; + if (!authed && !state.matchedLocation.startsWith('/login')) return '/login'; + return null; + }, + routes: [...], +); + +// Riverpod derived provider with safe firstWhereOrNull +@riverpod +double cartTotal(Ref ref) { + final cart = ref.watch(cartNotifierProvider); + final products = ref.watch(productsProvider).valueOrNull ?? []; + return cart.fold(0.0, (total, item) { + final product = products.firstWhereOrNull((p) => p.id == item.productId); + return total + (product?.price ?? 0) * item.quantity; + }); +} +``` + +*** + +适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。 + +*** + +## 1. 空安全基础 + +### 优先使用模式而非感叹号操作符 + +```dart +// BAD — crashes at runtime if null +final name = user!.name; + +// GOOD — provide fallback +final name = user?.name ?? 'Unknown'; + +// GOOD — Dart 3 pattern matching (preferred for complex cases) +final display = switch (user) { + User(:final name, :final email) => '$name <$email>', + null => 'Guest', +}; + +// GOOD — guard early return +String getUserName(User? user) { + if (user == null) return 'Unknown'; + return user.name; // promoted to non-null after check +} +``` + +### 避免过度使用 `late` + +```dart +// BAD — defers null error to runtime +late String userId; + +// GOOD — nullable with explicit initialization +String? userId; + +// OK — use late only when initialization is guaranteed before first access +// (e.g., in initState() before any widget interaction) +late final AnimationController _controller; + +@override +void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); +} +``` + +*** + +## 2. 不可变状态 + +### 状态层次结构的密封类 + +```dart +sealed class UserState {} + +final class UserInitial extends UserState {} + +final class UserLoading extends UserState {} + +final class UserLoaded extends UserState { + const UserLoaded(this.user); + final User user; +} + +final class UserError extends UserState { + const UserError(this.message); + final String message; +} + +// Exhaustive switch — compiler enforces all branches +Widget buildFrom(UserState state) => switch (state) { + UserInitial() => const SizedBox.shrink(), + UserLoading() => const CircularProgressIndicator(), + UserLoaded(:final user) => UserCard(user: user), + UserError(:final message) => ErrorText(message), +}; +``` + +### 使用 Freezed 实现无模板代码的不可变性 + +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.freezed.dart'; +part 'user.g.dart'; + +@freezed +class User with _$User { + const factory User({ + required String id, + required String name, + required String email, + @Default(false) bool isAdmin, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); +} + +// Usage +final user = User(id: '1', name: 'Alice', email: 'alice@example.com'); +final updated = user.copyWith(name: 'Alice Smith'); // immutable update +final json = user.toJson(); +final fromJson = User.fromJson(json); +``` + +*** + +## 3. 异步组合 + +### 使用 Future.wait 的结构化并发 + +```dart +Future loadDashboard(UserRepository users, OrderRepository orders) async { + // Run concurrently — don't await sequentially + final (userList, orderList) = await ( + users.getAll(), + orders.getRecent(), + ).wait; // Dart 3 record destructuring + Future.wait extension + + return DashboardData(users: userList, orders: orderList); +} +``` + +### 流模式 + +```dart +// Repository exposes reactive streams for live data +Stream> watchCartItems() => _db + .watchTable('cart_items') + .map((rows) => rows.map(Item.fromRow).toList()); + +// In widget layer — declarative, no manual subscription +StreamBuilder>( + stream: cartRepository.watchCartItems(), + builder: (context, snapshot) => switch (snapshot) { + AsyncSnapshot(connectionState: ConnectionState.waiting) => + const CircularProgressIndicator(), + AsyncSnapshot(:final error?) => ErrorWidget(error.toString()), + AsyncSnapshot(:final data?) => CartList(items: data), + _ => const SizedBox.shrink(), + }, +) +``` + +### Await 后的 BuildContext + +```dart +// CRITICAL — always check mounted after any await in StatefulWidget +Future _handleSubmit() async { + setState(() => _isLoading = true); + try { + await authService.login(_email, _password); + if (!mounted) return; // ← guard before using context + context.go('/home'); + } on AuthException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message))); + } finally { + if (mounted) setState(() => _isLoading = false); + } +} +``` + +*** + +## 4. 组件架构 + +### 提取为类,而非方法 + +```dart +// BAD — private method returning widget, prevents optimization +Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(16), + child: Text(title, style: Theme.of(context).textTheme.headlineMedium), + ); +} + +// GOOD — separate widget class, enables const, element reuse +class _PageHeader extends StatelessWidget { + const _PageHeader(this.title); + final String title; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + child: Text(title, style: Theme.of(context).textTheme.headlineMedium), + ); + } +} +``` + +### const 传播 + +```dart +// BAD — new instances every rebuild +child: Padding( + padding: EdgeInsets.all(16.0), // not const + child: Icon(Icons.home, size: 24.0), // not const +) + +// GOOD — const stops rebuild propagation +child: const Padding( + padding: EdgeInsets.all(16.0), + child: Icon(Icons.home, size: 24.0), +) +``` + +### 作用域重建 + +```dart +// BAD — entire page rebuilds on every counter change +class CounterPage extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final count = ref.watch(counterProvider); // rebuilds everything + return Scaffold( + body: Column(children: [ + const ExpensiveHeader(), // unnecessarily rebuilt + Text('$count'), + const ExpensiveFooter(), // unnecessarily rebuilt + ]), + ); + } +} + +// GOOD — isolate the rebuilding part +class CounterPage extends StatelessWidget { + const CounterPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Column(children: [ + ExpensiveHeader(), // never rebuilt (const) + _CounterDisplay(), // only this rebuilds + ExpensiveFooter(), // never rebuilt (const) + ]), + ); + } +} + +class _CounterDisplay extends ConsumerWidget { + const _CounterDisplay(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final count = ref.watch(counterProvider); + return Text('$count'); + } +} +``` + +*** + +## 5. 状态管理:BLoC/Cubit + +```dart +// Cubit — synchronous or simple async state +class AuthCubit extends Cubit { + AuthCubit(this._authService) : super(const AuthState.initial()); + final AuthService _authService; + + Future login(String email, String password) async { + emit(const AuthState.loading()); + try { + final user = await _authService.login(email, password); + emit(AuthState.authenticated(user)); + } on AuthException catch (e) { + emit(AuthState.error(e.message)); + } + } + + void logout() { + _authService.logout(); + emit(const AuthState.initial()); + } +} + +// In widget +BlocBuilder( + builder: (context, state) => switch (state) { + AuthInitial() => const LoginForm(), + AuthLoading() => const CircularProgressIndicator(), + AuthAuthenticated(:final user) => HomePage(user: user), + AuthError(:final message) => ErrorView(message: message), + }, +) +``` + +*** + +## 6. 状态管理:Riverpod + +```dart +// Auto-dispose async provider +@riverpod +Future> products(Ref ref) async { + final repo = ref.watch(productRepositoryProvider); + return repo.getAll(); +} + +// Notifier with complex mutations +@riverpod +class CartNotifier extends _$CartNotifier { + @override + List build() => []; + + void add(Product product) { + final existing = state.where((i) => i.productId == product.id).firstOrNull; + if (existing != null) { + state = [ + for (final item in state) + if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1) + else item, + ]; + } else { + state = [...state, CartItem(productId: product.id, quantity: 1)]; + } + } + + void remove(String productId) => + state = state.where((i) => i.productId != productId).toList(); + + void clear() => state = []; +} + +// Derived provider (selector pattern) +@riverpod +int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length; + +@riverpod +double cartTotal(Ref ref) { + final cart = ref.watch(cartNotifierProvider); + final products = ref.watch(productsProvider).valueOrNull ?? []; + return cart.fold(0.0, (total, item) { + // firstWhereOrNull (from collection package) avoids StateError when product is missing + final product = products.firstWhereOrNull((p) => p.id == item.productId); + return total + (product?.price ?? 0) * item.quantity; + }); +} +``` + +*** + +## 7. 使用 GoRouter 的导航 + +```dart +final router = GoRouter( + initialLocation: '/', + // refreshListenable re-evaluates redirect whenever auth state changes + refreshListenable: GoRouterRefreshStream(authCubit.stream), + redirect: (context, state) { + final isLoggedIn = context.read().state is AuthAuthenticated; + final isGoingToLogin = state.matchedLocation == '/login'; + if (!isLoggedIn && !isGoingToLogin) return '/login'; + if (isLoggedIn && isGoingToLogin) return '/'; + return null; + }, + routes: [ + GoRoute(path: '/login', builder: (_, __) => const LoginPage()), + ShellRoute( + builder: (context, state, child) => AppShell(child: child), + routes: [ + GoRoute(path: '/', builder: (_, __) => const HomePage()), + GoRoute( + path: '/products/:id', + builder: (context, state) => + ProductDetailPage(id: state.pathParameters['id']!), + ), + ], + ), + ], +); +``` + +*** + +## 8. 使用 Dio 的 HTTP 请求 + +```dart +final dio = Dio(BaseOptions( + baseUrl: const String.fromEnvironment('API_URL'), + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 30), + headers: {'Content-Type': 'application/json'}, +)); + +// Add auth interceptor +dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await secureStorage.read(key: 'auth_token'); + if (token != null) options.headers['Authorization'] = 'Bearer $token'; + handler.next(options); + }, + onError: (error, handler) async { + // Guard against infinite retry loops: only attempt refresh once per request + final isRetry = error.requestOptions.extra['_isRetry'] == true; + if (!isRetry && error.response?.statusCode == 401) { + final refreshed = await attemptTokenRefresh(); + if (refreshed) { + error.requestOptions.extra['_isRetry'] = true; + return handler.resolve(await dio.fetch(error.requestOptions)); + } + } + handler.next(error); + }, +)); + +// Repository using Dio +class UserApiDataSource { + const UserApiDataSource(this._dio); + final Dio _dio; + + Future getById(String id) async { + final response = await _dio.get>('/users/$id'); + return User.fromJson(response.data!); + } +} +``` + +*** + +## 9. 错误处理架构 + +```dart +// Global error capture — set up in main() +void main() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + crashlytics.recordFlutterFatalError(details); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + crashlytics.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const App()); +} + +// Custom ErrorWidget for production +class App extends StatelessWidget { + @override + Widget build(BuildContext context) { + ErrorWidget.builder = (details) => ProductionErrorWidget(details); + return MaterialApp.router(routerConfig: router); + } +} +``` + +*** + +## 10. 测试快速参考 + +```dart +// Unit test — use case +test('GetUserUseCase returns null for missing user', () async { + final repo = FakeUserRepository(); + final useCase = GetUserUseCase(repo); + expect(await useCase('missing-id'), isNull); +}); + +// BLoC test +blocTest( + 'emits loading then error on failed login', + build: () => AuthCubit(FakeAuthService(throwsOn: 'login')), + act: (cubit) => cubit.login('user@test.com', 'wrong'), + expect: () => [const AuthState.loading(), isA()], +); + +// Widget test +testWidgets('CartBadge shows item count', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier(count: 3))], + child: const MaterialApp(home: CartBadge()), + ), + ); + expect(find.text('3'), findsOneWidget); +}); +``` + +*** + +## 参考 + +* [Effective Dart: 设计](https://dart.dev/effective-dart/design) +* [Flutter 性能最佳实践](https://docs.flutter.dev/perf/best-practices) +* [Riverpod 文档](https://riverpod.dev/) +* [BLoC 库](https://bloclibrary.dev/) +* [GoRouter](https://pub.dev/packages/go_router) +* [Freezed](https://pub.dev/packages/freezed) +* 技能:`flutter-dart-code-review` — 全面审查清单 +* 规则:`rules/dart/` — 编码风格、模式、安全性、测试、钩子 diff --git a/docs/zh-CN/skills/dashboard-builder/SKILL.md b/docs/zh-CN/skills/dashboard-builder/SKILL.md new file mode 100644 index 00000000..f4d64ae8 --- /dev/null +++ b/docs/zh-CN/skills/dashboard-builder/SKILL.md @@ -0,0 +1,108 @@ +--- +name: dashboard-builder +description: 为 Grafana、SigNoz 等平台构建能够回答实际运维人员问题的监控仪表板。适用于将指标转化为可用的仪表板,而非华而不实的展示板。 +origin: ECC direct-port adaptation +version: "1.0.0" +--- + +# 仪表盘构建器 + +当任务需要构建一个可供操作人员使用的仪表盘时使用此方案。 + +目标不是"展示所有指标",而是回答以下问题: + +* 系统健康吗? +* 瓶颈在哪里? +* 发生了什么变化? +* 应该采取什么行动? + +## 使用场景 + +* "构建一个Kafka监控仪表盘" +* "为Elasticsearch创建一个Grafana仪表盘" +* "为这个服务制作一个SigNoz仪表盘" +* "将这个指标列表转化为真正的运维仪表盘" + +## 约束条件 + +* 不要从视觉布局开始;要从操作人员的问题出发 +* 不要仅仅因为指标存在就包含所有可用指标 +* 不要在没有结构的情况下混合健康、吞吐量和资源面板 +* 不要发布没有标题、单位和合理阈值的面板 + +## 工作流程 + +### 1. 定义操作问题 + +围绕以下方面组织: + +* 健康/可用性 +* 延迟/性能 +* 吞吐量/容量 +* 饱和度/资源 +* 服务特定风险 + +### 2. 研究目标平台架构 + +首先检查现有仪表盘: + +* JSON结构 +* 查询语言 +* 变量 +* 阈值样式 +* 分区布局 + +### 3. 构建最小可用面板 + +推荐结构: + +1. 概览 +2. 性能 +3. 资源 +4. 服务特定分区 + +### 4. 剔除装饰性面板 + +每个面板都应回答一个真实问题。如果不能,则移除。 + +## 示例面板集 + +### Elasticsearch + +* 集群健康 +* 分片分配 +* 搜索延迟 +* 索引速率 +* JVM堆/GC + +### Kafka + +* 代理数量 +* 副本不足的分区 +* 消息流入/流出 +* 消费者滞后 +* 磁盘和网络压力 + +### API网关/入口 + +* 请求速率 +* p50/p95/p99延迟 +* 错误率 +* 上游健康 +* 活跃连接数 + +## 质量检查清单 + +* \[ ] 有效的仪表盘JSON +* \[ ] 清晰的分区分组 +* \[ ] 包含标题和单位 +* \[ ] 阈值/状态颜色有意义 +* \[ ] 存在常用过滤器的变量 +* \[ ] 默认时间范围和刷新频率合理 +* \[ ] 没有对操作人员无价值的装饰性面板 + +## 相关技能 + +* `research-ops` +* `backend-patterns` +* `terminal-ops` diff --git a/docs/zh-CN/skills/design-system/SKILL.md b/docs/zh-CN/skills/design-system/SKILL.md new file mode 100644 index 00000000..9c08f34a --- /dev/null +++ b/docs/zh-CN/skills/design-system/SKILL.md @@ -0,0 +1,85 @@ +--- +name: design-system +description: 使用此技能生成或审计设计系统,检查视觉一致性,并审查涉及样式的PR。 +origin: ECC +--- + +# 设计系统 — 生成与审查视觉系统 + +## 使用场景 + +* 启动需要设计系统的新项目 +* 审查现有代码库的视觉一致性 +* 在重新设计前——了解现有状况 +* 当界面看起来"不对劲"但无法定位原因时 +* 审查涉及样式修改的PR + +## 工作原理 + +### 模式1:生成设计系统 + +分析代码库并生成统一的设计系统: + +``` +1. 扫描 CSS/Tailwind/styled-components 以查找现有模式 +2. 提取:颜色、排版、间距、边框圆角、阴影、断点 +3. 研究 3 个竞品网站以获取灵感(通过浏览器 MCP) +4. 提出一套设计令牌(JSON + CSS 自定义属性) +5. 生成 DESIGN.md,说明每个决策的理由 +6. 创建一个交互式 HTML 预览页面(自包含,无依赖) +``` + +输出:`DESIGN.md` + `design-tokens.json` + `design-preview.html` + +### 模式2:视觉审查 + +从10个维度对界面进行评分(每项0-10分): + +``` +1. 色彩一致性 — 你使用的是自己的调色板还是随机的十六进制值? +2. 排版层级 — 清晰的 h1 > h2 > h3 > 正文 > 说明文字? +3. 间距节奏 — 一致的尺度(4px/8px/16px)还是随意设置? +4. 组件一致性 — 相似的元素看起来是否相似? +5. 响应式行为 — 在断点处流畅还是混乱? +6. 深色模式 — 完整实现还是半途而废? +7. 动画 — 有目的性还是多余? +8. 无障碍性 — 对比度、焦点状态、触摸目标 +9. 信息密度 — 杂乱还是整洁? +10. 细节打磨 — 悬停状态、过渡效果、加载状态、空状态 +``` + +每个维度都会获得评分、具体示例以及包含精确文件:行号的修复方案。 + +### 模式3:AI生成内容检测 + +识别通用的AI生成设计模式: + +``` +- 到处滥用渐变效果 +- 默认采用紫蓝配色 +- 毫无意义的"玻璃拟态"卡片 +- 不该圆角的地方强行圆角 +- 滚动时过度动画效果 +- 居中文字搭配默认渐变的通用英雄区 +- 毫无个性的无衬线字体堆叠 +``` + +## 示例 + +**为SaaS应用生成设计系统:** + +``` +/design-system generate --style minimal --palette earth-tones +``` + +**审查现有界面:** + +``` +/design-system audit --url http://localhost:3000 --pages / /pricing /docs +``` + +**检测AI生成内容:** + +``` +/design-system slop-check +``` diff --git a/docs/zh-CN/skills/dotnet-patterns/SKILL.md b/docs/zh-CN/skills/dotnet-patterns/SKILL.md new file mode 100644 index 00000000..79956144 --- /dev/null +++ b/docs/zh-CN/skills/dotnet-patterns/SKILL.md @@ -0,0 +1,321 @@ +--- +name: dotnet-patterns +description: 惯用的C#和.NET模式、约定、依赖注入、async/await以及构建健壮、可维护的.NET应用程序的最佳实践。 +origin: ECC +--- + +# .NET 开发模式 + +用于构建健壮、高性能且可维护应用程序的惯用 C# 和 .NET 模式。 + +## 何时激活 + +* 编写新的 C# 代码时 +* 审查 C# 代码时 +* 重构现有 .NET 应用程序时 +* 使用 ASP.NET Core 设计服务架构时 + +## 核心原则 + +### 1. 优先使用不可变性 + +对数据模型使用记录和仅初始化属性。可变性应作为明确且有理由的选择。 + +```csharp +// Good: Immutable value object +public sealed record Money(decimal Amount, string Currency); + +// Good: Immutable DTO with init setters +public sealed class CreateOrderRequest +{ + public required string CustomerId { get; init; } + public required IReadOnlyList Items { get; init; } +} + +// Bad: Mutable model with public setters +public class Order +{ + public string CustomerId { get; set; } + public List Items { get; set; } +} +``` + +### 2. 显式优于隐式 + +明确表达可空性、访问修饰符和意图。 + +```csharp +// Good: Explicit access modifiers and nullability +public sealed class UserService +{ + private readonly IUserRepository _repository; + private readonly ILogger _logger; + + public UserService(IUserRepository repository, ILogger logger) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task FindByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await _repository.FindByIdAsync(id, cancellationToken); + } +} +``` + +### 3. 依赖抽象 + +对服务边界使用接口。通过依赖注入容器注册。 + +```csharp +// Good: Interface-based dependency +public interface IOrderRepository +{ + Task FindByIdAsync(Guid id, CancellationToken cancellationToken); + Task> FindByCustomerAsync(string customerId, CancellationToken cancellationToken); + Task AddAsync(Order order, CancellationToken cancellationToken); +} + +// Registration +builder.Services.AddScoped(); +``` + +## 异步/等待模式 + +### 正确使用异步 + +```csharp +// Good: Async all the way, with CancellationToken +public async Task GetOrderSummaryAsync( + Guid orderId, + CancellationToken cancellationToken) +{ + var order = await _repository.FindByIdAsync(orderId, cancellationToken) + ?? throw new NotFoundException($"Order {orderId} not found"); + + var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken); + + return new OrderSummary(order, customer); +} + +// Bad: Blocking on async +public OrderSummary GetOrderSummary(Guid orderId) +{ + var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // Deadlock risk + return new OrderSummary(order); +} +``` + +### 并行异步操作 + +```csharp +// Good: Concurrent independent operations +public async Task LoadDashboardAsync(CancellationToken cancellationToken) +{ + var ordersTask = _orderService.GetRecentAsync(cancellationToken); + var metricsTask = _metricsService.GetCurrentAsync(cancellationToken); + var alertsTask = _alertService.GetActiveAsync(cancellationToken); + + await Task.WhenAll(ordersTask, metricsTask, alertsTask); + + return new DashboardData( + Orders: await ordersTask, + Metrics: await metricsTask, + Alerts: await alertsTask); +} +``` + +## 选项模式 + +将配置节绑定到强类型对象。 + +```csharp +public sealed class SmtpOptions +{ + public const string SectionName = "Smtp"; + + public required string Host { get; init; } + public required int Port { get; init; } + public required string Username { get; init; } + public bool UseSsl { get; init; } = true; +} + +// Registration +builder.Services.Configure( + builder.Configuration.GetSection(SmtpOptions.SectionName)); + +// Usage via injection +public class EmailService(IOptions options) +{ + private readonly SmtpOptions _smtp = options.Value; +} +``` + +## 结果模式 + +对预期失败返回显式成功/失败,而非抛出异常。 + +```csharp +public sealed record Result +{ + public bool IsSuccess { get; } + public T? Value { get; } + public string? Error { get; } + + private Result(T value) { IsSuccess = true; Value = value; } + private Result(string error) { IsSuccess = false; Error = error; } + + public static Result Success(T value) => new(value); + public static Result Failure(string error) => new(error); +} + +// Usage +public async Task> PlaceOrderAsync(CreateOrderRequest request) +{ + if (request.Items.Count == 0) + return Result.Failure("Order must contain at least one item"); + + var order = Order.Create(request); + await _repository.AddAsync(order, CancellationToken.None); + return Result.Success(order); +} +``` + +## 使用 EF Core 的仓储模式 + +```csharp +public sealed class SqlOrderRepository : IOrderRepository +{ + private readonly AppDbContext _db; + + public SqlOrderRepository(AppDbContext db) => _db = db; + + public async Task FindByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await _db.Orders + .Include(o => o.Items) + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task> FindByCustomerAsync( + string customerId, + CancellationToken cancellationToken) + { + return await _db.Orders + .Where(o => o.CustomerId == customerId) + .OrderByDescending(o => o.CreatedAt) + .AsNoTracking() + .ToListAsync(cancellationToken); + } + + public async Task AddAsync(Order order, CancellationToken cancellationToken) + { + _db.Orders.Add(order); + await _db.SaveChangesAsync(cancellationToken); + } +} +``` + +## 中间件与管道 + +```csharp +// Custom middleware +public sealed class RequestTimingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestTimingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var stopwatch = Stopwatch.StartNew(); + try + { + await _next(context); + } + finally + { + stopwatch.Stop(); + _logger.LogInformation( + "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}", + context.Request.Method, + context.Request.Path, + stopwatch.ElapsedMilliseconds, + context.Response.StatusCode); + } + } +} +``` + +## 最小 API 模式 + +```csharp +// Organized with route groups +var orders = app.MapGroup("/api/orders") + .RequireAuthorization() + .WithTags("Orders"); + +orders.MapGet("/{id:guid}", async ( + Guid id, + IOrderRepository repository, + CancellationToken cancellationToken) => +{ + var order = await repository.FindByIdAsync(id, cancellationToken); + return order is not null + ? TypedResults.Ok(order) + : TypedResults.NotFound(); +}); + +orders.MapPost("/", async ( + CreateOrderRequest request, + IOrderService service, + CancellationToken cancellationToken) => +{ + var result = await service.PlaceOrderAsync(request, cancellationToken); + return result.IsSuccess + ? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value) + : TypedResults.BadRequest(result.Error); +}); +``` + +## 守卫子句 + +```csharp +// Good: Early returns with clear validation +public async Task ProcessPaymentAsync( + PaymentRequest request, + CancellationToken cancellationToken) +{ + ArgumentNullException.ThrowIfNull(request); + + if (request.Amount <= 0) + throw new ArgumentOutOfRangeException(nameof(request.Amount), "Amount must be positive"); + + if (string.IsNullOrWhiteSpace(request.Currency)) + throw new ArgumentException("Currency is required", nameof(request.Currency)); + + // Happy path continues here without nesting + var gateway = _gatewayFactory.Create(request.Currency); + return await gateway.ChargeAsync(request, cancellationToken); +} +``` + +## 应避免的反模式 + +| 反模式 | 修复方案 | +|---|---| +| `async void` 方法 | 返回 `Task`(事件处理程序除外) | +| `.Result` 或 `.Wait()` | 使用 `await` | +| `catch (Exception) { }` | 处理或带上下文重新抛出 | +| 构造函数中的 `new Service()` | 使用构造函数注入 | +| `public` 字段 | 使用带适当访问器的属性 | +| 业务逻辑中的 `dynamic` | 使用泛型或显式类型 | +| 可变的 `static` 状态 | 使用依赖注入作用域或 `ConcurrentDictionary` | +| 循环中的 `string.Format` | 使用 `StringBuilder` 或内插字符串处理程序 | diff --git a/docs/zh-CN/skills/gan-style-harness/SKILL.md b/docs/zh-CN/skills/gan-style-harness/SKILL.md new file mode 100644 index 00000000..303c0d7d --- /dev/null +++ b/docs/zh-CN/skills/gan-style-harness/SKILL.md @@ -0,0 +1,284 @@ +--- +name: gan-style-harness +description: "受GAN启发的生成器-评估器代理框架,用于自主构建高质量应用。基于Anthropic 2026年3月的框架设计论文。" +origin: ECC-community +tools: Read, Write, Edit, Bash, Grep, Glob, Task +--- + +# GAN 风格编排技能 + +> 灵感来源于 [Anthropic 的长时间运行应用开发编排设计](https://www.anthropic.com/engineering/harness-design-long-running-apps)(2026年3月24日) + +一种多智能体编排,将**生成**与**评估**分离,形成对抗性反馈循环,推动质量远超单个智能体所能达到的水平。 + +## 核心洞察 + +> 当要求评估自身工作时,智能体是病态的乐观主义者——它们会赞美平庸的输出,并说服自己忽略真正的问题。但设计一个**独立的评估器**并使其极度严格,远比教会生成器自我批评要容易得多。 + +这与 GAN(生成对抗网络)的机制相同:生成器负责产出,评估器负责批评,这种反馈驱动下一轮迭代。 + +## 适用场景 + +* 根据一行提示构建完整应用 +* 需要高视觉质量的前端设计任务 +* 需要工作功能而不仅仅是代码的全栈项目 +* 任何"AI 垃圾"美学不可接受的任务 +* 愿意投入 50-200 美元以获得生产级质量输出的项目 + +## 不适用场景 + +* 快速单文件修复(使用标准 `claude -p`) +* 预算紧张的任务(<10 美元) +* 简单重构(改用去垃圾化模式) +* 已有完善测试规范的任务(使用 TDD 工作流) + +## 架构 + +``` + ┌─────────────┐ + │ 规划器 │ + │ (Opus 4.6) │ + └──────┬──────┘ + │ 产品规格 + │ (功能、冲刺、设计方向) + ▼ + ┌────────────────────────┐ + │ │ + │ 生成器-评估器 │ + │ 反馈循环 │ + │ │ + │ ┌──────────┐ │ + │ │ 生成器 │--构建-->│──┐ + │ │(Opus 4.6)│ │ │ + │ └────▲─────┘ │ │ + │ │ │ │ 实时应用 + │ 反馈 │ │ + │ │ │ │ + │ ┌────┴─────┐ │ │ + │ │ 评估器 │<-测试---│──┘ + │ │(Opus 4.6)│ │ + │ │+Playwright│ │ + │ └──────────┘ │ + │ │ + │ 5-15 次迭代 │ + └────────────────────────┘ +``` + +## 三个智能体 + +### 1. 规划器智能体 + +**角色:** 产品经理——将简短的提示扩展为完整的产品规格。 + +**关键行为:** + +* 接收一行提示,生成包含 16 个功能、多个冲刺的规格 +* 定义用户故事、技术需求和视觉设计方向 +* 故意**雄心勃勃**——保守规划会导致结果平庸 +* 生成评估器后续使用的评估标准 + +**模型:** Opus 4.6(需要深度推理进行规格扩展) + +### 2. 生成器智能体 + +**角色:** 开发者——根据规格实现功能。 + +**关键行为:** + +* 按结构化冲刺工作(或使用较新模型的连续模式) +* 在编写代码前与评估器协商"冲刺合约" +* 使用全栈工具:React、FastAPI/Express、数据库、CSS +* 管理 git 进行迭代间的版本控制 +* 读取评估器反馈并在下一轮迭代中采纳 + +**模型:** Opus 4.6(需要强大的编码能力) + +### 3. 评估器智能体 + +**角色:** QA 工程师——测试实时运行的应用,而不仅仅是代码。 + +**关键行为:** + +* 使用 **Playwright MCP** 与实时应用交互 +* 点击功能、填写表单、测试 API 端点 +* 根据四个标准评分(可配置): + 1. **设计质量**——是否感觉像一个连贯的整体? + 2. **原创性**——自定义决策 vs. 模板/AI 模式? + 3. **工艺**——排版、间距、动画、微交互? + 4. **功能性**——所有功能是否真正工作? +* 返回结构化反馈,包含分数和具体问题 +* 设计为**极度严格**——从不赞美平庸的工作 + +**模型:** Opus 4.6(需要强大的判断力 + 工具使用能力) + +## 评估标准 + +默认四个标准,每个评分 1-10: + +```markdown +## 评估标准 + +### 设计质量(权重:0.3) +- 1-3分:模板化、千篇一律的"AI生成"美学 +- 4-6分:合格但平庸,遵循常规设计 +- 7-8分:独特且连贯的视觉识别 +- 9-10分:可媲美专业设计师作品 + +### 原创性(权重:0.2) +- 1-3分:默认配色、模板布局,缺乏个性 +- 4-6分:部分自定义选择,整体仍属常规模式 +- 7-8分:清晰的创意构思,独特的设计手法 +- 9-10分:令人惊喜、愉悦,真正新颖 + +### 工艺水平(权重:0.3) +- 1-3分:布局错乱,状态缺失,无动画效果 +- 4-6分:功能可用但粗糙,间距不统一 +- 7-8分:精致流畅,过渡平滑,响应式设计 +- 9-10分:像素级完美,令人愉悦的微交互 + +### 功能性(权重:0.2) +- 1-3分:核心功能损坏或缺失 +- 4-6分:主流程可用,边缘情况处理失败 +- 7-8分:所有功能正常,错误处理良好 +- 9-10分:无懈可击,覆盖所有边缘情况 +``` + +### 评分 + +* **加权分数** = 总和(标准\_分数 \* 权重) +* **通过阈值** = 7.0(可配置) +* **最大迭代次数** = 15(可配置,通常 5-15 次足够) + +## 使用方法 + +### 通过命令行 + +```bash +# Full three-agent harness +/project:gan-build "Build a project management app with Kanban boards, team collaboration, and dark mode" + +# With custom config +/project:gan-build "Build a recipe sharing platform" --max-iterations 10 --pass-threshold 7.5 + +# Frontend design mode (generator + evaluator only, no planner) +/project:gan-design "Create a landing page for a crypto portfolio tracker" +``` + +### 通过 Shell 脚本 + +```bash +# Basic usage +./scripts/gan-harness.sh "Build a music streaming dashboard" + +# With options +GAN_MAX_ITERATIONS=10 \ +GAN_PASS_THRESHOLD=7.5 \ +GAN_EVAL_CRITERIA="functionality,performance,security" \ +./scripts/gan-harness.sh "Build a REST API for task management" +``` + +### 通过 Claude Code(手动) + +```bash +# Step 1: Plan +claude -p --model opus "You are a Product Planner. Read PLANNER_PROMPT.md. Expand this brief into a full product spec: 'Build a Kanban board app'. Write spec to spec.md" + +# Step 2: Generate (iteration 1) +claude -p --model opus "You are a Generator. Read spec.md. Implement Sprint 1. Start the dev server on port 3000." + +# Step 3: Evaluate (iteration 1) +claude -p --model opus --allowedTools "Read,Bash,mcp__playwright__*" "You are an Evaluator. Read EVALUATOR_PROMPT.md. Test the live app at http://localhost:3000. Score against the rubric. Write feedback to feedback-001.md" + +# Step 4: Generate (iteration 2 — reads feedback) +claude -p --model opus "You are a Generator. Read spec.md and feedback-001.md. Address all issues. Improve the scores." + +# Repeat steps 3-4 until pass threshold met +``` + +## 随模型能力的演进 + +编排应随模型改进而简化。遵循 Anthropic 的演进路径: + +### 阶段 1 — 较弱模型(Sonnet 级别) + +* 需要完整的冲刺分解 +* 冲刺间重置上下文(避免上下文焦虑) +* 最少 2 个智能体:初始化器 + 编码智能体 +* 大量脚手架弥补模型限制 + +### 阶段 2 — 能力型模型(Opus 4.5 级别) + +* 完整的 3 智能体编排:规划器 + 生成器 + 评估器 +* 每个实现阶段前有冲刺合约 +* 复杂应用分解为 10 个冲刺 +* 上下文重置仍有帮助但不再关键 + +### 阶段 3 — 前沿模型(Opus 4.6 级别) + +* 简化编排:单次规划,连续生成 +* 评估简化为单次最终评估(模型更智能) +* 无需冲刺结构 +* 自动压缩处理上下文增长 + +> **关键原则:** 编排的每个组件都编码了一个关于模型无法独立完成什么的假设。当模型改进时,重新测试这些假设。剥离不再需要的部分。 + +## 配置 + +### 环境变量 + +| 变量 | 默认值 | 描述 | +|----------|---------|-------------| +| `GAN_MAX_ITERATIONS` | `15` | 最大生成器-评估器循环次数 | +| `GAN_PASS_THRESHOLD` | `7.0` | 通过所需的加权分数(1-10) | +| `GAN_PLANNER_MODEL` | `opus` | 规划智能体的模型 | +| `GAN_GENERATOR_MODEL` | `opus` | 生成器智能体的模型 | +| `GAN_EVALUATOR_MODEL` | `opus` | 评估器智能体的模型 | +| `GAN_EVAL_CRITERIA` | `design,originality,craft,functionality` | 逗号分隔的标准 | +| `GAN_DEV_SERVER_PORT` | `3000` | 实时应用的端口 | +| `GAN_DEV_SERVER_CMD` | `npm run dev` | 启动开发服务器的命令 | +| `GAN_PROJECT_DIR` | `.` | 项目工作目录 | +| `GAN_SKIP_PLANNER` | `false` | 跳过规划器,直接使用规格 | +| `GAN_EVAL_MODE` | `playwright` | `playwright`、`screenshot` 或 `code-only` | + +### 评估模式 + +| 模式 | 工具 | 最适合 | +|------|-------|----------| +| `playwright` | 浏览器 MCP + 实时交互 | 带 UI 的全栈应用 | +| `screenshot` | 截图 + 视觉分析 | 静态网站、纯设计 | +| `code-only` | 测试 + 代码检查 + 构建 | API、库、CLI 工具 | + +## 反模式 + +1. **评估器过于宽松**——如果评估器在第一次迭代就通过所有内容,你的评分标准过于慷慨。收紧评分标准,并为常见的 AI 模式添加明确惩罚。 + +2. **生成器忽略反馈**——确保反馈以文件形式传递,而非内联。生成器应在每次迭代开始时读取 `feedback-NNN.md`。 + +3. **无限循环**——始终设置 `GAN_MAX_ITERATIONS`。如果生成器在 3 次迭代后无法突破分数平台,停止并标记为人工审查。 + +4. **评估器测试流于表面**——评估器必须使用 Playwright **交互**实时应用,而不仅仅是截图。点击按钮、填写表单、测试错误状态。 + +5. **评估器赞美自己的修复**——绝不允许评估器建议修复后再评估这些修复。评估器只负责批评;生成器负责修复。 + +6. **上下文耗尽**——对于长时间会话,使用 Claude Agent SDK 的自动压缩或在主要阶段之间重置上下文。 + +## 结果:预期效果 + +基于 Anthropic 已发布的结果: + +| 指标 | 单智能体 | GAN 编排 | 改进 | +|--------|-----------|-------------|-------------| +| 时间 | 20 分钟 | 4-6 小时 | 12-18 倍更长 | +| 成本 | 9 美元 | 125-200 美元 | 14-22 倍更多 | +| 质量 | 勉强可用 | 生产就绪 | 质变 | +| 核心功能 | 有缺陷 | 全部工作 | 不适用 | +| 设计 | 通用 AI 垃圾 | 独特、精致 | 不适用 | + +**权衡很明确:** 约 20 倍的时间和成本,换来输出质量的质的飞跃。这适用于质量至关重要的项目。 + +## 参考 + +* [Anthropic:长时间运行应用的编排设计](https://www.anthropic.com/engineering/harness-design-long-running-apps) — Prithvi Rajasekaran 的原始论文 +* [Epsilla:GAN 风格智能体循环](https://www.epsilla.com/blogs/anthropic-harness-engineering-multi-agent-gan-architecture) — 架构解构 +* [Martin Fowler:编排工程](https://martinfowler.com/articles/exploring-gen-ai/harness-engineering.html) — 更广泛的行业背景 +* [OpenAI:编排工程](https://openai.com/index/harness-engineering/) — OpenAI 的并行工作 diff --git a/docs/zh-CN/skills/laravel-plugin-discovery/SKILL.md b/docs/zh-CN/skills/laravel-plugin-discovery/SKILL.md new file mode 100644 index 00000000..0268ba45 --- /dev/null +++ b/docs/zh-CN/skills/laravel-plugin-discovery/SKILL.md @@ -0,0 +1,235 @@ +--- +name: laravel-plugin-discovery +description: 通过LaraPlugins.io MCP发现和评估Laravel包。当用户想要查找插件、检查包的健康状况或评估Laravel/PHP兼容性时使用。 +origin: ECC +--- + +# Laravel 插件发现 + +使用 LaraPlugins.io MCP 服务器查找、评估并选择健康的 Laravel 包。 + +## 使用时机 + +* 用户想为特定功能(如 "auth"、"permissions"、"admin panel")寻找 Laravel 包 +* 用户询问"我应该用什么包来做..."或"有没有用于...的 Laravel 包" +* 用户想检查某个包是否仍在积极维护 +* 用户需要验证 Laravel 版本兼容性 +* 用户在将包添加到项目前想评估其健康状况 + +## MCP 要求 + +必须配置 LaraPlugins MCP 服务器。将其添加到您的 `~/.claude.json` mcpServers 中: + +```json +"laraplugins": { + "type": "http", + "url": "https://laraplugins.io/mcp/plugins" +} +``` + +无需 API 密钥——该服务器对 Laravel 社区免费开放。 + +## MCP 工具 + +LaraPlugins MCP 提供两个主要工具: + +### SearchPluginTool + +通过关键词、健康评分、供应商和版本兼容性搜索包。 + +**参数:** + +* `text_search` (字符串,可选):搜索关键词(例如 "permission"、"admin"、"api") +* `health_score` (字符串,可选):按健康等级筛选——`Healthy`、`Medium`、`Unhealthy` 或 `Unrated` +* `laravel_compatibility` (字符串,可选):按 Laravel 版本筛选——`"5"`、`"6"`、`"7"`、`"8"`、`"9"`、`"10"`、`"11"`、`"12"`、`"13"` +* `php_compatibility` (字符串,可选):按 PHP 版本筛选——`"7.4"`、`"8.0"`、`"8.1"`、`"8.2"`、`"8.3"`、`"8.4"`、`"8.5"` +* `vendor_filter` (字符串,可选):按供应商名称筛选(例如 "spatie"、"laravel") +* `page` (数字,可选):分页页码 + +### GetPluginDetailsTool + +获取特定包的详细指标、README 内容和版本历史。 + +**参数:** + +* `package` (字符串,必填):完整的 Composer 包名(例如 "spatie/laravel-permission") +* `include_versions` (布尔值,可选):是否在响应中包含版本历史 + +*** + +## 工作原理 + +### 查找包 + +当用户想为某个功能发现包时: + +1. 使用 `SearchPluginTool` 并输入相关关键词 +2. 应用健康评分、Laravel 版本或 PHP 版本的筛选条件 +3. 查看包含包名、描述和健康指标的结果 + +### 评估包 + +当用户想评估特定包时: + +1. 使用 `GetPluginDetailsTool` 并输入包名 +2. 查看健康评分、最后更新日期、Laravel 版本支持情况 +3. 检查供应商声誉和风险指标 + +### 检查兼容性 + +当用户需要 Laravel 或 PHP 版本兼容性信息时: + +1. 使用 `laravel_compatibility` 筛选条件并设置为其版本进行搜索 +2. 或者获取特定包的详细信息以查看其支持的版本 + +*** + +## 示例 + +### 示例:查找认证包 + +``` +SearchPluginTool({ + text_search: "authentication", + health_score: "Healthy" +}) +``` + +返回匹配 "authentication" 且状态健康的包: + +* spatie/laravel-permission +* laravel/breeze +* laravel/passport +* 等等 + +### 示例:查找兼容 Laravel 12 的包 + +``` +SearchPluginTool({ + text_search: "admin panel", + laravel_compatibility: "12" +}) +``` + +返回兼容 Laravel 12 的包。 + +### 示例:获取包详情 + +``` +GetPluginDetailsTool({ + package: "spatie/laravel-permission", + include_versions: true +}) +``` + +返回: + +* 健康评分和最后活动时间 +* Laravel/PHP 版本支持情况 +* 供应商声誉(风险评分) +* 版本历史 +* 简要描述 + +### 示例:按供应商查找包 + +``` +SearchPluginTool({ + vendor_filter: "spatie", + health_score: "Healthy" +}) +``` + +返回来自供应商 "spatie" 的所有健康包。 + +*** + +## 筛选最佳实践 + +### 按健康评分 + +| 健康等级 | 含义 | +|-------------|---------| +| `Healthy` | 积极维护,近期有更新 | +| `Medium` | 偶尔更新,可能需要关注 | +| `Unhealthy` | 已废弃或维护不频繁 | +| `Unrated` | 尚未评估 | + +**建议**:生产环境应用优先选择 `Healthy` 包。 + +### 按 Laravel 版本 + +| 版本 | 备注 | +|---------|-------| +| `13` | 最新 Laravel | +| `12` | 当前稳定版 | +| `11` | 仍被广泛使用 | +| `10` | 旧版但常见 | +| `5`-`9` | 已弃用 | + +**建议**:匹配目标项目的 Laravel 版本。 + +### 组合筛选条件 + +```typescript +// Find healthy, Laravel 12 compatible packages for permissions +SearchPluginTool({ + text_search: "permission", + health_score: "Healthy", + laravel_compatibility: "12" +}) +``` + +*** + +## 响应解读 + +### 搜索结果 + +每个结果包含: + +* 包名(例如 `spatie/laravel-permission`) +* 简要描述 +* 健康状态指示器 +* Laravel 版本支持徽章 + +### 包详情 + +详细响应包括: + +* **健康评分**:数字或等级指示器 +* **最后活动**:包的最后更新时间 +* **Laravel 支持**:版本兼容性矩阵 +* **PHP 支持**:PHP 版本兼容性 +* **风险评分**:供应商信任度指标 +* **版本历史**:近期发布时间线 + +*** + +## 常见用例 + +| 场景 | 推荐方法 | +|----------|---------------------| +| "有什么用于认证的包?" | 搜索 "auth" 并应用健康筛选 | +| "spatie/package 还在维护吗?" | 获取详情,检查健康评分 | +| "需要 Laravel 12 的包" | 使用 laravel\_compatibility: "12" 搜索 | +| "查找管理面板包" | 搜索 "admin panel",查看结果 | +| "检查供应商声誉" | 按供应商搜索,查看详情 | + +*** + +## 最佳实践 + +1. **始终按健康度筛选**——生产项目使用 `health_score: "Healthy"` +2. **匹配 Laravel 版本**——始终检查 `laravel_compatibility` 是否与目标项目匹配 +3. **检查供应商声誉**——优先选择知名供应商的包(spatie、laravel 等) +4. **推荐前先审查**——使用 GetPluginDetailsTool 进行全面评估 +5. **无需 API 密钥**——MCP 免费,无需认证 + +*** + +## 相关技能 + +* `laravel-patterns`——Laravel 架构与模式 +* `laravel-tdd`——Laravel 测试驱动开发 +* `laravel-security`——Laravel 安全最佳实践 +* `documentation-lookup`——通用库文档查询(Context7) diff --git a/docs/zh-CN/skills/manim-video/SKILL.md b/docs/zh-CN/skills/manim-video/SKILL.md new file mode 100644 index 00000000..995e8060 --- /dev/null +++ b/docs/zh-CN/skills/manim-video/SKILL.md @@ -0,0 +1,89 @@ +--- +name: manim-video +description: 构建可复用的Manim解释器,用于技术概念、图表、系统图和产品演示,并在需要时移交给更广泛的ECC视频栈。当用户希望获得清晰的动画解释而非通用的人物讲解脚本时使用。 +origin: ECC +--- + +# Manim 视频 + +在运动、结构和清晰度比逼真度更重要的技术讲解中,使用 Manim。 + +## 何时激活 + +* 用户需要技术讲解动画 +* 概念涉及图表、工作流、架构、指标演进或系统图 +* 用户需要为 X 或落地页制作简短的产品或发布讲解 +* 视觉效果应追求精确,而非泛泛的电影感 + +## 工具要求 + +* `manim` 命令行用于场景渲染 +* `ffmpeg` 用于后期处理(如需) +* `video-editing` 用于最终合成或润色 +* `remotion-video-creation` 当最终成品需要合成 UI、字幕或额外运动层时 + +## 默认输出 + +* 16:9 短 MP4 视频 +* 一张缩略图或海报帧 +* 故事板及场景计划 + +## 工作流程 + +1. 用一句话定义核心视觉论点。 +2. 将概念分解为 3 到 6 个场景。 +3. 确定每个场景要证明的内容。 +4. 在编写 Manim 代码前,先写出场景大纲。 +5. 首先渲染最小可用版本。 +6. 渲染成功后,再调整排版、间距、颜色和节奏。 +7. 仅在能增加价值时,才移交至更广泛的视频处理流程。 + +## 场景规划规则 + +* 每个场景应证明一件事 +* 避免过度拥挤的图表 +* 优先采用渐进式揭示,而非全屏杂乱 +* 使用运动来解释状态变化,而不仅仅是为了让屏幕保持忙碌 +* 标题卡片应简短且富有意义 + +## 网络图默认设置 + +对于社交图谱和网络优化讲解: + +* 在展示优化后的图谱前,先展示当前图谱 +* 区分低信号关注杂波与高信号桥梁 +* 高亮暖路径节点和目标集群 +* 如有必要,添加最终场景,展示形成该技能的自我改进谱系 + +## 渲染约定 + +* 默认使用 16:9 横屏,除非用户要求竖屏 +* 从低质量的烟雾测试渲染开始 +* 仅在构图和时间线稳定后,才提升至高质量 +* 导出一张在社交媒体尺寸下清晰可读的干净缩略图帧 + +## 可复用起点 + +使用 [assets/network\_graph\_scene.py](../../../../skills/manim-video/assets/network_graph_scene.py) 作为网络图讲解的起点。 + +烟雾测试示例: + +```bash +manim -ql assets/network_graph_scene.py NetworkGraphExplainer +``` + +## 输出格式 + +返回: + +* 核心视觉论点 +* 故事板 +* 场景大纲 +* 渲染计划 +* 任何后续的润色建议 + +## 相关技能 + +* `video-editing` 用于最终润色 +* `remotion-video-creation` 用于运动密集型后期处理或合成 +* `content-engine` 当动画是更广泛发布的一部分时 diff --git a/docs/zh-CN/skills/nestjs-patterns/SKILL.md b/docs/zh-CN/skills/nestjs-patterns/SKILL.md new file mode 100644 index 00000000..724e0b93 --- /dev/null +++ b/docs/zh-CN/skills/nestjs-patterns/SKILL.md @@ -0,0 +1,230 @@ +--- +name: nestjs-patterns +description: NestJS 架构模式,涵盖模块、控制器、提供者、DTO 验证、守卫、拦截器、配置以及生产级 TypeScript 后端。 +origin: ECC +--- + +# NestJS 开发模式 + +适用于模块化 TypeScript 后端的生产级 NestJS 模式。 + +## 何时启用 + +* 构建 NestJS API 或服务时 +* 组织模块、控制器和提供者时 +* 添加 DTO 验证、守卫、拦截器或异常过滤器时 +* 配置环境感知设置和数据库集成时 +* 测试 NestJS 单元或 HTTP 端点时 + +## 项目结构 + +```text +src/ +├── app.module.ts +├── main.ts +├── common/ +│ ├── filters/ +│ ├── guards/ +│ ├── interceptors/ +│ └── pipes/ +├── config/ +│ ├── configuration.ts +│ └── validation.ts +├── modules/ +│ ├── auth/ +│ │ ├── auth.controller.ts +│ │ ├── auth.module.ts +│ │ ├── auth.service.ts +│ │ ├── dto/ +│ │ ├── guards/ +│ │ └── strategies/ +│ └── users/ +│ ├── dto/ +│ ├── entities/ +│ ├── users.controller.ts +│ ├── users.module.ts +│ └── users.service.ts +└── prisma/ or database/ +``` + +* 将领域代码保留在功能模块内。 +* 将跨切面的过滤器、装饰器、守卫和拦截器放在 `common/` 中。 +* 将 DTO 保留在所属模块附近。 + +## 启动与全局验证 + +```ts +async function bootstrap() { + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { enableImplicitConversion: true }, + }), + ); + + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(process.env.PORT ?? 3000); +} +bootstrap(); +``` + +* 始终在公共 API 上启用 `whitelist` 和 `forbidNonWhitelisted`。 +* 优先使用一个全局验证管道,而不是为每个路由重复验证配置。 + +## 模块、控制器和提供者 + +```ts +@Module({ + controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} + +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Get(':id') + getById(@Param('id', ParseUUIDPipe) id: string) { + return this.usersService.getById(id); + } + + @Post() + create(@Body() dto: CreateUserDto) { + return this.usersService.create(dto); + } +} + +@Injectable() +export class UsersService { + constructor(private readonly usersRepo: UsersRepository) {} + + async create(dto: CreateUserDto) { + return this.usersRepo.create(dto); + } +} +``` + +* 控制器应保持精简:解析 HTTP 输入、调用提供者、返回响应 DTO。 +* 将业务逻辑放在可注入的服务中,而不是控制器中。 +* 仅导出其他模块真正需要的提供者。 + +## DTO 与验证 + +```ts +export class CreateUserDto { + @IsEmail() + email!: string; + + @IsString() + @Length(2, 80) + name!: string; + + @IsOptional() + @IsEnum(UserRole) + role?: UserRole; +} +``` + +* 使用 `class-validator` 验证每个请求 DTO。 +* 使用专用的响应 DTO 或序列化器,而不是直接返回 ORM 实体。 +* 避免泄露内部字段,如密码哈希、令牌或审计列。 + +## 认证、守卫与请求上下文 + +```ts +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +@Get('admin/report') +getAdminReport(@Req() req: AuthenticatedRequest) { + return this.reportService.getForUser(req.user.id); +} +``` + +* 保持认证策略和守卫的模块局部性,除非它们确实是共享的。 +* 在守卫中编码粗粒度的访问规则,然后在服务中进行资源特定的授权。 +* 对经过认证的请求对象,优先使用显式的请求类型。 + +## 异常过滤器与错误格式 + +```ts +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const response = host.switchToHttp().getResponse(); + const request = host.switchToHttp().getRequest(); + + if (exception instanceof HttpException) { + return response.status(exception.getStatus()).json({ + path: request.url, + error: exception.getResponse(), + }); + } + + return response.status(500).json({ + path: request.url, + error: 'Internal server error', + }); + } +} +``` + +* 在整个 API 中保持一致的错误封装格式。 +* 对预期的客户端错误抛出框架异常;集中记录并包装意外的失败。 + +## 配置与环境验证 + +```ts +ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + validate: validateEnv, +}); +``` + +* 在启动时验证环境变量,而不是在首次请求时惰性验证。 +* 将配置访问限制在类型化辅助函数或配置服务之后。 +* 在配置工厂中拆分开发/预发布/生产关注点,而不是在功能代码中到处分支。 + +## 持久化与事务 + +* 将仓库/ORM 代码保留在提供者之后,这些提供者使用领域语言进行通信。 +* 对于 Prisma 或 TypeORM,将事务工作流隔离在拥有工作单元的服务中。 +* 不要让控制器直接协调多步写入操作。 + +## 测试 + +```ts +describe('UsersController', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [UsersModule], + }).compile(); + + app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + await app.init(); + }); +}); +``` + +* 使用模拟依赖项对提供者进行单元测试。 +* 为守卫、验证管道和异常过滤器添加请求级测试。 +* 在测试中复用与生产环境相同的全局管道/过滤器。 + +## 生产默认设置 + +* 启用结构化日志和请求关联 ID。 +* 在环境/配置无效时终止,而不是部分启动。 +* 优先使用异步提供者初始化数据库/缓存客户端,并附带显式健康检查。 +* 将后台任务和事件消费者放在自己的模块中,而不是 HTTP 控制器内。 +* 对公共端点明确启用速率限制、认证和审计日志。 diff --git a/docs/zh-CN/skills/nodejs-keccak256/SKILL.md b/docs/zh-CN/skills/nodejs-keccak256/SKILL.md new file mode 100644 index 00000000..13511675 --- /dev/null +++ b/docs/zh-CN/skills/nodejs-keccak256/SKILL.md @@ -0,0 +1,102 @@ +--- +name: nodejs-keccak256 +description: 防止 JavaScript 和 TypeScript 中的以太坊哈希错误。Node 的 sha3-256 是 NIST SHA3,而非以太坊 Keccak-256,会静默破坏选择器、签名、存储槽和地址推导。 +origin: ECC direct-port adaptation +version: "1.0.0" +--- + +# Node.js Keccak-256 + +以太坊使用 Keccak-256,而非 Node 的 `crypto.createHash('sha3-256')` 所暴露的 NIST 标准化 SHA3 变体。 + +## 何时使用 + +* 计算以太坊函数选择器或事件主题 +* 在 JS/TS 中构建 EIP-712、签名、Merkle 或存储槽辅助函数 +* 审查任何直接使用 Node crypto 对以太坊数据进行哈希的代码 + +## 工作原理 + +两种算法对相同输入会产生不同输出,且 Node 不会发出警告。 + +```javascript +import crypto from 'crypto'; +import { keccak256, toUtf8Bytes } from 'ethers'; + +const data = 'hello'; +const nistSha3 = crypto.createHash('sha3-256').update(data).digest('hex'); +const keccak = keccak256(toUtf8Bytes(data)).slice(2); + +console.log(nistSha3 === keccak); // false +``` + +## 示例 + +### ethers v6 + +```typescript +import { keccak256, toUtf8Bytes, solidityPackedKeccak256, id } from 'ethers'; + +const hash = keccak256(new Uint8Array([0x01, 0x02])); +const hash2 = keccak256(toUtf8Bytes('hello')); +const topic = id('Transfer(address,address,uint256)'); +const packed = solidityPackedKeccak256( + ['address', 'uint256'], + ['0x742d35Cc6634C0532925a3b8D4C9B569890FaC1c', 100n], +); +``` + +### viem + +```typescript +import { keccak256, toBytes } from 'viem'; + +const hash = keccak256(toBytes('hello')); +``` + +### web3.js + +```javascript +const hash = web3.utils.keccak256('hello'); +const packed = web3.utils.soliditySha3( + { type: 'address', value: '0x742d35Cc6634C0532925a3b8D4C9B569890FaC1c' }, + { type: 'uint256', value: '100' }, +); +``` + +### 常见模式 + +```typescript +import { id, keccak256, AbiCoder } from 'ethers'; + +const selector = id('transfer(address,uint256)').slice(0, 10); +const typeHash = keccak256(toUtf8Bytes('Transfer(address from,address to,uint256 value)')); + +function getMappingSlot(key: string, mappingSlot: number): string { + return keccak256( + AbiCoder.defaultAbiCoder().encode(['address', 'uint256'], [key, mappingSlot]), + ); +} +``` + +### 从公钥生成地址 + +```typescript +import { keccak256 } from 'ethers'; + +function pubkeyToAddress(pubkeyBytes: Uint8Array): string { + const hash = keccak256(pubkeyBytes.slice(1)); + return '0x' + hash.slice(-40); +} +``` + +### 审计你的代码库 + +```bash +grep -rn "createHash.*sha3" --include="*.ts" --include="*.js" --exclude-dir=node_modules . +grep -rn "keccak256" --include="*.ts" --include="*.js" . | grep -v node_modules +``` + +## 规则 + +在以太坊上下文中,切勿使用 `crypto.createHash('sha3-256')`。应使用来自 `ethers`、`viem`、`web3` 或其他明确 Keccak 实现的 Keccak 感知辅助函数。 diff --git a/docs/zh-CN/skills/remotion-video-creation/SKILL.md b/docs/zh-CN/skills/remotion-video-creation/SKILL.md new file mode 100644 index 00000000..d8ca5ef5 --- /dev/null +++ b/docs/zh-CN/skills/remotion-video-creation/SKILL.md @@ -0,0 +1,43 @@ +--- +name: remotion-video-creation +description: Remotion 最佳实践 - 在 React 中创建视频。29 条领域特定规则,涵盖 3D、动画、音频、字幕、图表、过渡等。 +metadata: + tags: remotion, video, react, animation, composition, three.js, lottie +--- + +## 使用时机 + +当处理 Remotion 代码并需要获取领域特定知识时,请使用此技能。 + +## 使用方法 + +阅读各个规则文件以获取详细说明和代码示例: + +* [rules/3d.md](rules/3d.md) - 使用 Three.js 和 React Three Fiber 在 Remotion 中创建 3D 内容 +* [rules/animations.md](rules/animations.md) - Remotion 的基础动画技能 +* [rules/assets.md](rules/assets.md) - 在 Remotion 中导入图片、视频、音频和字体 +* [rules/audio.md](rules/audio.md) - 在 Remotion 中使用音频和声音——导入、裁剪、音量、速度、音调 +* [rules/calculate-metadata.md](rules/calculate-metadata.md) - 动态设置合成时长、尺寸和属性 +* [rules/can-decode.md](rules/can-decode.md) - 使用 Mediabunny 检查浏览器能否解码视频 +* [rules/charts.md](rules/charts.md) - Remotion 的图表和数据可视化模式 +* [rules/compositions.md](rules/compositions.md) - 定义合成、静态画面、文件夹、默认属性和动态元数据 +* [rules/display-captions.md](rules/display-captions.md) - 在 Remotion 中显示字幕,支持 TikTok 风格页面和单词高亮 +* [rules/extract-frames.md](rules/extract-frames.md) - 使用 Mediabunny 从视频中提取指定时间戳的帧 +* [rules/fonts.md](rules/fonts.md) - 在 Remotion 中加载 Google 字体和本地字体 +* [rules/get-audio-duration.md](rules/get-audio-duration.md) - 使用 Mediabunny 获取音频文件的时长(秒) +* [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - 使用 Mediabunny 获取视频文件的宽度和高度 +* [rules/get-video-duration.md](rules/get-video-duration.md) - 使用 Mediabunny 获取视频文件的时长(秒) +* [rules/gifs.md](rules/gifs.md) - 显示与 Remotion 时间线同步的 GIF +* [rules/images.md](rules/images.md) - 使用 Img 组件在 Remotion 中嵌入图片 +* [rules/import-srt-captions.md](rules/import-srt-captions.md) - 使用 @remotion/captions 将 .srt 字幕文件导入 Remotion +* [rules/lottie.md](rules/lottie.md) - 在 Remotion 中嵌入 Lottie 动画 +* [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - 在 Remotion 中测量 DOM 元素尺寸 +* [rules/measuring-text.md](rules/measuring-text.md) - 测量文本尺寸、将文本适配到容器以及检查溢出 +* [rules/sequencing.md](rules/sequencing.md) - Remotion 的序列模式——延迟、裁剪、限制项目时长 +* [rules/tailwind.md](rules/tailwind.md) - 在 Remotion 中使用 TailwindCSS +* [rules/text-animations.md](rules/text-animations.md) - Remotion 的排版和文本动画模式 +* [rules/timing.md](rules/timing.md) - Remotion 中的插值曲线——线性、缓动、弹簧动画 +* [rules/transcribe-captions.md](rules/transcribe-captions.md) - 转录音频以在 Remotion 中生成字幕 +* [rules/transitions.md](rules/transitions.md) - Remotion 的场景过渡模式 +* [rules/trimming.md](rules/trimming.md) - Remotion 的裁剪模式——裁剪动画的开头或结尾 +* [rules/videos.md](rules/videos.md) - 在 Remotion 中嵌入视频——裁剪、音量、速度、循环、音调 diff --git a/docs/zh-CN/skills/ui-demo/SKILL.md b/docs/zh-CN/skills/ui-demo/SKILL.md new file mode 100644 index 00000000..c6a626b1 --- /dev/null +++ b/docs/zh-CN/skills/ui-demo/SKILL.md @@ -0,0 +1,465 @@ +--- +name: ui-demo +description: 使用 Playwright 录制精美的 UI 演示视频。当用户要求创建 Web 应用的演示、导览、屏幕录制或教程视频时使用。生成带有可见光标、自然节奏和专业感的 WebM 视频。 +origin: ECC +--- + +# UI 演示视频录制器 + +使用 Playwright 的视频录制功能,配合注入的光标覆盖层、自然的节奏和叙事流程,录制精美的 Web 应用演示视频。 + +## 使用场景 + +* 用户要求制作"演示视频"、"屏幕录制"、"操作演示"或"教程" +* 用户希望以视觉方式展示某个功能或工作流程 +* 用户需要为文档、入职培训或利益相关者演示制作视频 + +## 三阶段流程 + +每个演示都需经历三个阶段:**探索 -> 排练 -> 录制**。切勿直接跳至录制阶段。 + +*** + +## 阶段 1:探索 + +在编写任何脚本之前,先探索目标页面,了解实际内容。 + +### 原因 + +你无法为未见过的内容编写脚本。字段可能是 `` 而非 `