mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-12 15:47:27 +08:00
docs: salvage zh-CN framework skill translations
This commit is contained in:
committed by
Affaan Mustafa
parent
3242ed461f
commit
4359947a6a
321
docs/zh-CN/skills/csharp-testing/SKILL.md
Normal file
321
docs/zh-CN/skills/csharp-testing/SKILL.md
Normal file
@@ -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<IOrderRepository>();
|
||||
private readonly ILogger<OrderService> _logger = Substitute.For<ILogger<OrderService>>();
|
||||
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<CreateOrderRequest, string> 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<CancellationToken>())
|
||||
.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<Order>(o => o.CustomerId == request.CustomerId),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
```
|
||||
|
||||
## ASP.NET Core 集成测试
|
||||
|
||||
### WebApplicationFactory 设置
|
||||
|
||||
```csharp
|
||||
public sealed class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public OrderApiTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_client = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Replace real DB with in-memory for tests
|
||||
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
||||
services.AddDbContext<AppDbContext>(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<AppDbContext>()
|
||||
.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<OrderItem> _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/
|
||||
```
|
||||
565
docs/zh-CN/skills/dart-flutter-patterns/SKILL.md
Normal file
565
docs/zh-CN/skills/dart-flutter-patterns/SKILL.md
Normal file
@@ -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<T> {}
|
||||
final class Loading<T> extends AsyncState<T> {}
|
||||
final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }
|
||||
final class Failure<T> extends AsyncState<T> { 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<AuthCubit>().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<String, dynamic> 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<DashboardData> 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<List<Item>> watchCartItems() => _db
|
||||
.watchTable('cart_items')
|
||||
.map((rows) => rows.map(Item.fromRow).toList());
|
||||
|
||||
// In widget layer — declarative, no manual subscription
|
||||
StreamBuilder<List<Item>>(
|
||||
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<void> _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<AuthState> {
|
||||
AuthCubit(this._authService) : super(const AuthState.initial());
|
||||
final AuthService _authService;
|
||||
|
||||
Future<void> 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<AuthCubit, AuthState>(
|
||||
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<List<Product>> products(Ref ref) async {
|
||||
final repo = ref.watch(productRepositoryProvider);
|
||||
return repo.getAll();
|
||||
}
|
||||
|
||||
// Notifier with complex mutations
|
||||
@riverpod
|
||||
class CartNotifier extends _$CartNotifier {
|
||||
@override
|
||||
List<CartItem> 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<AuthCubit>().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<User> getById(String id) async {
|
||||
final response = await _dio.get<Map<String, dynamic>>('/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<AuthCubit, AuthState>(
|
||||
'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<AuthError>()],
|
||||
);
|
||||
|
||||
// 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/` — 编码风格、模式、安全性、测试、钩子
|
||||
108
docs/zh-CN/skills/dashboard-builder/SKILL.md
Normal file
108
docs/zh-CN/skills/dashboard-builder/SKILL.md
Normal file
@@ -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`
|
||||
85
docs/zh-CN/skills/design-system/SKILL.md
Normal file
85
docs/zh-CN/skills/design-system/SKILL.md
Normal file
@@ -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
|
||||
```
|
||||
321
docs/zh-CN/skills/dotnet-patterns/SKILL.md
Normal file
321
docs/zh-CN/skills/dotnet-patterns/SKILL.md
Normal file
@@ -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<OrderItem> Items { get; init; }
|
||||
}
|
||||
|
||||
// Bad: Mutable model with public setters
|
||||
public class Order
|
||||
{
|
||||
public string CustomerId { get; set; }
|
||||
public List<OrderItem> Items { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 显式优于隐式
|
||||
|
||||
明确表达可空性、访问修饰符和意图。
|
||||
|
||||
```csharp
|
||||
// Good: Explicit access modifiers and nullability
|
||||
public sealed class UserService
|
||||
{
|
||||
private readonly IUserRepository _repository;
|
||||
private readonly ILogger<UserService> _logger;
|
||||
|
||||
public UserService(IUserRepository repository, ILogger<UserService> logger)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _repository.FindByIdAsync(id, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 依赖抽象
|
||||
|
||||
对服务边界使用接口。通过依赖注入容器注册。
|
||||
|
||||
```csharp
|
||||
// Good: Interface-based dependency
|
||||
public interface IOrderRepository
|
||||
{
|
||||
Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
|
||||
Task AddAsync(Order order, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
// Registration
|
||||
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
|
||||
```
|
||||
|
||||
## 异步/等待模式
|
||||
|
||||
### 正确使用异步
|
||||
|
||||
```csharp
|
||||
// Good: Async all the way, with CancellationToken
|
||||
public async Task<OrderSummary> 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<DashboardData> 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<SmtpOptions>(
|
||||
builder.Configuration.GetSection(SmtpOptions.SectionName));
|
||||
|
||||
// Usage via injection
|
||||
public class EmailService(IOptions<SmtpOptions> options)
|
||||
{
|
||||
private readonly SmtpOptions _smtp = options.Value;
|
||||
}
|
||||
```
|
||||
|
||||
## 结果模式
|
||||
|
||||
对预期失败返回显式成功/失败,而非抛出异常。
|
||||
|
||||
```csharp
|
||||
public sealed record Result<T>
|
||||
{
|
||||
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<T> Success(T value) => new(value);
|
||||
public static Result<T> Failure(string error) => new(error);
|
||||
}
|
||||
|
||||
// Usage
|
||||
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
|
||||
{
|
||||
if (request.Items.Count == 0)
|
||||
return Result<Order>.Failure("Order must contain at least one item");
|
||||
|
||||
var order = Order.Create(request);
|
||||
await _repository.AddAsync(order, CancellationToken.None);
|
||||
return Result<Order>.Success(order);
|
||||
}
|
||||
```
|
||||
|
||||
## 使用 EF Core 的仓储模式
|
||||
|
||||
```csharp
|
||||
public sealed class SqlOrderRepository : IOrderRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public SqlOrderRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _db.Orders
|
||||
.Include(o => o.Items)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Order>> 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<RequestTimingMiddleware> _logger;
|
||||
|
||||
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> 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<ProcessResult> 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` 或内插字符串处理程序 |
|
||||
284
docs/zh-CN/skills/gan-style-harness/SKILL.md
Normal file
284
docs/zh-CN/skills/gan-style-harness/SKILL.md
Normal file
@@ -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 的并行工作
|
||||
235
docs/zh-CN/skills/laravel-plugin-discovery/SKILL.md
Normal file
235
docs/zh-CN/skills/laravel-plugin-discovery/SKILL.md
Normal file
@@ -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)
|
||||
89
docs/zh-CN/skills/manim-video/SKILL.md
Normal file
89
docs/zh-CN/skills/manim-video/SKILL.md
Normal file
@@ -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` 当动画是更广泛发布的一部分时
|
||||
230
docs/zh-CN/skills/nestjs-patterns/SKILL.md
Normal file
230
docs/zh-CN/skills/nestjs-patterns/SKILL.md
Normal file
@@ -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<Response>();
|
||||
const request = host.switchToHttp().getRequest<Request>();
|
||||
|
||||
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 控制器内。
|
||||
* 对公共端点明确启用速率限制、认证和审计日志。
|
||||
102
docs/zh-CN/skills/nodejs-keccak256/SKILL.md
Normal file
102
docs/zh-CN/skills/nodejs-keccak256/SKILL.md
Normal file
@@ -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 感知辅助函数。
|
||||
43
docs/zh-CN/skills/remotion-video-creation/SKILL.md
Normal file
43
docs/zh-CN/skills/remotion-video-creation/SKILL.md
Normal file
@@ -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 中嵌入视频——裁剪、音量、速度、循环、音调
|
||||
465
docs/zh-CN/skills/ui-demo/SKILL.md
Normal file
465
docs/zh-CN/skills/ui-demo/SKILL.md
Normal file
@@ -0,0 +1,465 @@
|
||||
---
|
||||
name: ui-demo
|
||||
description: 使用 Playwright 录制精美的 UI 演示视频。当用户要求创建 Web 应用的演示、导览、屏幕录制或教程视频时使用。生成带有可见光标、自然节奏和专业感的 WebM 视频。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# UI 演示视频录制器
|
||||
|
||||
使用 Playwright 的视频录制功能,配合注入的光标覆盖层、自然的节奏和叙事流程,录制精美的 Web 应用演示视频。
|
||||
|
||||
## 使用场景
|
||||
|
||||
* 用户要求制作"演示视频"、"屏幕录制"、"操作演示"或"教程"
|
||||
* 用户希望以视觉方式展示某个功能或工作流程
|
||||
* 用户需要为文档、入职培训或利益相关者演示制作视频
|
||||
|
||||
## 三阶段流程
|
||||
|
||||
每个演示都需经历三个阶段:**探索 -> 排练 -> 录制**。切勿直接跳至录制阶段。
|
||||
|
||||
***
|
||||
|
||||
## 阶段 1:探索
|
||||
|
||||
在编写任何脚本之前,先探索目标页面,了解实际内容。
|
||||
|
||||
### 原因
|
||||
|
||||
你无法为未见过的内容编写脚本。字段可能是 `<input>` 而非 `<textarea>`,下拉菜单可能是自定义组件而非 `<select>`,评论框可能支持 `@mentions` 或 `#tags`。假设会无声地破坏录制。
|
||||
|
||||
### 方法
|
||||
|
||||
导航至流程中的每个页面,并转储其交互元素:
|
||||
|
||||
```javascript
|
||||
// Run this for each page in the flow BEFORE writing the demo script
|
||||
const fields = await page.evaluate(() => {
|
||||
const els = [];
|
||||
document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
|
||||
if (el.offsetParent !== null) {
|
||||
els.push({
|
||||
tag: el.tagName,
|
||||
type: el.type || '',
|
||||
name: el.name || '',
|
||||
placeholder: el.placeholder || '',
|
||||
text: el.textContent?.trim().substring(0, 40) || '',
|
||||
contentEditable: el.contentEditable === 'true',
|
||||
role: el.getAttribute('role') || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
return els;
|
||||
});
|
||||
console.log(JSON.stringify(fields, null, 2));
|
||||
```
|
||||
|
||||
### 需要关注的内容
|
||||
|
||||
* **表单字段**:它们是 `<select>`、`<input>`、自定义下拉菜单还是组合框?
|
||||
* **选择选项**:转储选项的值和文本。占位符通常包含 `value="0"` 或 `value=""`,看起来非空。使用 `Array.from(el.options).map(o => ({ value: o.value, text: o.text }))`。跳过文本包含"选择"或值为 `"0"` 的选项。
|
||||
* **富文本**:评论框是否支持 `@mentions`、`#tags`、Markdown 或表情符号?检查占位符文本。
|
||||
* **必填字段**:哪些字段会阻止表单提交?检查标签中的 `required`、`*`,并尝试提交空表单以查看验证错误。
|
||||
* **动态内容**:字段是否在填写其他字段后出现?
|
||||
* **按钮标签**:确切的文本,如 `"Submit"`、`"Submit Request"` 或 `"Send"`。
|
||||
* **表格列标题**:对于表格驱动的模态框,将每个 `input[type="number"]` 映射到其列标题,而不是假设所有数字输入都表示相同含义。
|
||||
|
||||
### 输出
|
||||
|
||||
每个页面的字段映射,用于在脚本中编写正确的选择器。示例:
|
||||
|
||||
```text
|
||||
/purchase-requests/new:
|
||||
- 预算代码: <select> (页面上的第一个下拉框,4个选项)
|
||||
- 期望交付日期: <input type="date">
|
||||
- 背景说明: <textarea> (非输入框)
|
||||
- BOM表: 可内联编辑的单元格,包含 span.cursor-pointer -> input 模式
|
||||
- 提交: <button> 文本="提交"
|
||||
|
||||
/purchase-requests/N (详情):
|
||||
- 评论: <input placeholder="输入消息..."> 支持 @用户 和 #PR 标签
|
||||
- 发送: <button> 文本="发送" (在输入内容前处于禁用状态)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 阶段 2:排练
|
||||
|
||||
在不录制的情况下运行所有步骤。验证每个选择器都能解析。
|
||||
|
||||
### 原因
|
||||
|
||||
静默的选择器失败是演示录制中断的主要原因。排练可以在浪费录制之前发现它们。
|
||||
|
||||
### 方法
|
||||
|
||||
使用 `ensureVisible`,一个记录日志并大声报错的包装器:
|
||||
|
||||
```javascript
|
||||
async function ensureVisible(page, locator, label) {
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
const msg = `REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}`;
|
||||
console.error(msg);
|
||||
const found = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
|
||||
.filter(el => el.offsetParent !== null)
|
||||
.map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
|
||||
.join('\n ');
|
||||
});
|
||||
console.error(' Visible elements:\n ' + found);
|
||||
return false;
|
||||
}
|
||||
console.log(`REHEARSAL OK: "${label}"`);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 排练脚本结构
|
||||
|
||||
```javascript
|
||||
const steps = [
|
||||
{ label: 'Login email field', selector: '#email' },
|
||||
{ label: 'Login submit', selector: 'button[type="submit"]' },
|
||||
{ label: 'New Request button', selector: 'button:has-text("New Request")' },
|
||||
{ label: 'Budget Code select', selector: 'select' },
|
||||
{ label: 'Delivery date', selector: 'input[type="date"]:visible' },
|
||||
{ label: 'Description field', selector: 'textarea:visible' },
|
||||
{ label: 'Add Item button', selector: 'button:has-text("Add Item")' },
|
||||
{ label: 'Submit button', selector: 'button:has-text("Submit")' },
|
||||
];
|
||||
|
||||
let allOk = true;
|
||||
for (const step of steps) {
|
||||
if (!await ensureVisible(page, step.selector, step.label)) {
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
if (!allOk) {
|
||||
console.error('REHEARSAL FAILED - fix selectors before recording');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('REHEARSAL PASSED - all selectors verified');
|
||||
```
|
||||
|
||||
### 排练失败时
|
||||
|
||||
1. 读取可见元素转储。
|
||||
2. 找到正确的选择器。
|
||||
3. 更新脚本。
|
||||
4. 重新运行排练。
|
||||
5. 仅在所有选择器通过后才继续。
|
||||
|
||||
***
|
||||
|
||||
## 阶段 3:录制
|
||||
|
||||
仅在探索和排练通过后,才创建录制。
|
||||
|
||||
### 录制原则
|
||||
|
||||
#### 1. 叙事流程
|
||||
|
||||
将视频规划为一个故事。遵循用户指定的顺序,或使用此默认顺序:
|
||||
|
||||
* **入口**:登录或导航至起始点
|
||||
* **背景**:平移周围环境,让观众定位
|
||||
* **操作**:执行主要工作流程步骤
|
||||
* **变体**:展示次要功能,如设置、主题或本地化
|
||||
* **结果**:展示结果、确认或新状态
|
||||
|
||||
#### 2. 节奏
|
||||
|
||||
* 登录后:`4s`
|
||||
* 导航后:`3s`
|
||||
* 点击按钮后:`2s`
|
||||
* 主要步骤之间:`1.5-2s`
|
||||
* 最终操作后:`3s`
|
||||
* 输入延迟:每个字符 `25-40ms`
|
||||
|
||||
#### 3. 光标覆盖层
|
||||
|
||||
注入一个跟随鼠标移动的 SVG 箭头光标:
|
||||
|
||||
```javascript
|
||||
async function injectCursor(page) {
|
||||
await page.evaluate(() => {
|
||||
if (document.getElementById('demo-cursor')) return;
|
||||
const cursor = document.createElement('div');
|
||||
cursor.id = 'demo-cursor';
|
||||
cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
cursor.style.cssText = `
|
||||
position: fixed; z-index: 999999; pointer-events: none;
|
||||
width: 24px; height: 24px;
|
||||
transition: left 0.1s, top 0.1s;
|
||||
filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
|
||||
`;
|
||||
cursor.style.left = '0px';
|
||||
cursor.style.top = '0px';
|
||||
document.body.appendChild(cursor);
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
cursor.style.left = e.clientX + 'px';
|
||||
cursor.style.top = e.clientY + 'px';
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
每次页面导航后调用 `injectCursor(page)`,因为覆盖层会在导航时被销毁。
|
||||
|
||||
#### 4. 鼠标移动
|
||||
|
||||
切勿瞬移光标。在点击前移动到目标:
|
||||
|
||||
```javascript
|
||||
async function moveAndClick(page, locator, label, opts = {}) {
|
||||
const { postClickDelay = 800, ...clickOpts } = opts;
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
console.error(`WARNING: moveAndClick skipped - "${label}" not visible`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await el.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
const box = await el.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
await el.click(clickOpts);
|
||||
} catch (e) {
|
||||
console.error(`WARNING: moveAndClick failed on "${label}": ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
await page.waitForTimeout(postClickDelay);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
每次调用都应包含描述性的 `label` 以便调试。
|
||||
|
||||
#### 5. 输入
|
||||
|
||||
可见地输入,而非瞬间填充:
|
||||
|
||||
```javascript
|
||||
async function typeSlowly(page, locator, text, label, charDelay = 35) {
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
console.error(`WARNING: typeSlowly skipped - "${label}" not visible`);
|
||||
return false;
|
||||
}
|
||||
await moveAndClick(page, el, label);
|
||||
await el.fill('');
|
||||
await el.pressSequentially(text, { delay: charDelay });
|
||||
await page.waitForTimeout(500);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. 滚动
|
||||
|
||||
使用平滑滚动而非跳跃:
|
||||
|
||||
```javascript
|
||||
await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
|
||||
await page.waitForTimeout(1500);
|
||||
```
|
||||
|
||||
#### 7. 仪表盘平移
|
||||
|
||||
展示仪表盘或概览页面时,将光标移过关键元素:
|
||||
|
||||
```javascript
|
||||
async function panElements(page, selector, maxCount = 6) {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
|
||||
try {
|
||||
const box = await elements[i].boundingBox();
|
||||
if (box && box.y < 700) {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`WARNING: panElements skipped element ${i} (selector: "${selector}"): ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 8. 字幕
|
||||
|
||||
在视口底部注入一个字幕栏:
|
||||
|
||||
```javascript
|
||||
async function injectSubtitleBar(page) {
|
||||
await page.evaluate(() => {
|
||||
if (document.getElementById('demo-subtitle')) return;
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'demo-subtitle';
|
||||
bar.style.cssText = `
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
|
||||
text-align: center; padding: 12px 24px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: white; font-family: -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
`;
|
||||
bar.textContent = '';
|
||||
bar.style.opacity = '0';
|
||||
document.body.appendChild(bar);
|
||||
});
|
||||
}
|
||||
|
||||
async function showSubtitle(page, text) {
|
||||
await page.evaluate((t) => {
|
||||
const bar = document.getElementById('demo-subtitle');
|
||||
if (!bar) return;
|
||||
if (t) {
|
||||
bar.textContent = t;
|
||||
bar.style.opacity = '1';
|
||||
} else {
|
||||
bar.style.opacity = '0';
|
||||
}
|
||||
}, text);
|
||||
if (text) await page.waitForTimeout(800);
|
||||
}
|
||||
```
|
||||
|
||||
每次导航后,将 `injectSubtitleBar(page)` 与 `injectCursor(page)` 一起调用。
|
||||
|
||||
使用模式:
|
||||
|
||||
```javascript
|
||||
await showSubtitle(page, 'Step 1 - Logging in');
|
||||
await showSubtitle(page, 'Step 2 - Dashboard overview');
|
||||
await showSubtitle(page, '');
|
||||
```
|
||||
|
||||
指南:
|
||||
|
||||
* 保持字幕文本简短,最好在 60 个字符以内。
|
||||
* 使用 `Step N - Action` 格式以保持一致性。
|
||||
* 在长时间暂停且界面可以自我说明时清除字幕。
|
||||
|
||||
## 脚本模板
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
|
||||
const VIDEO_DIR = path.join(__dirname, 'screenshots');
|
||||
const OUTPUT_NAME = 'demo-FEATURE.webm';
|
||||
const REHEARSAL = process.argv.includes('--rehearse');
|
||||
|
||||
// Paste injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
|
||||
// typeSlowly, ensureVisible, and panElements here.
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
if (REHEARSAL) {
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
const page = await context.newPage();
|
||||
// Navigate through the flow and run ensureVisible for each selector.
|
||||
await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await browser.newContext({
|
||||
recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
|
||||
viewport: { width: 1280, height: 720 }
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await injectCursor(page);
|
||||
await injectSubtitleBar(page);
|
||||
|
||||
await showSubtitle(page, 'Step 1 - Logging in');
|
||||
// login actions
|
||||
|
||||
await page.goto(`${BASE_URL}/dashboard`);
|
||||
await injectCursor(page);
|
||||
await injectSubtitleBar(page);
|
||||
await showSubtitle(page, 'Step 2 - Dashboard overview');
|
||||
// pan dashboard
|
||||
|
||||
await showSubtitle(page, 'Step 3 - Main workflow');
|
||||
// action sequence
|
||||
|
||||
await showSubtitle(page, 'Step 4 - Result');
|
||||
// final reveal
|
||||
await showSubtitle(page, '');
|
||||
} catch (err) {
|
||||
console.error('DEMO ERROR:', err.message);
|
||||
} finally {
|
||||
await context.close();
|
||||
const video = page.video();
|
||||
if (video) {
|
||||
const src = await video.path();
|
||||
const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log('Video saved:', dest);
|
||||
} catch (e) {
|
||||
console.error('ERROR: Failed to copy video:', e.message);
|
||||
console.error(' Source:', src);
|
||||
console.error(' Destination:', dest);
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
使用方式:
|
||||
|
||||
```bash
|
||||
# Phase 2: Rehearse
|
||||
node demo-script.cjs --rehearse
|
||||
|
||||
# Phase 3: Record
|
||||
node demo-script.cjs
|
||||
```
|
||||
|
||||
## 录制前检查清单
|
||||
|
||||
* \[ ] 探索阶段已完成
|
||||
* \[ ] 排练通过,所有选择器正常
|
||||
* \[ ] 已启用无头模式
|
||||
* \[ ] 分辨率设置为 `1280x720`
|
||||
* \[ ] 每次导航后重新注入光标和字幕覆盖层
|
||||
* \[ ] 在主要过渡时使用 `showSubtitle(page, 'Step N - ...')`
|
||||
* \[ ] 所有点击均使用 `moveAndClick` 并带有描述性标签
|
||||
* \[ ] 可见输入使用 `typeSlowly`
|
||||
* \[ ] 无静默捕获;辅助函数记录警告
|
||||
* \[ ] 内容展示使用平滑滚动
|
||||
* \[ ] 关键暂停对观看者可见
|
||||
* \[ ] 流程符合请求的故事顺序
|
||||
* \[ ] 脚本反映阶段 1 中发现的实际 UI
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
1. 导航后光标消失 - 重新注入。
|
||||
2. 视频太快 - 添加暂停。
|
||||
3. 光标是点而非箭头 - 使用 SVG 覆盖层。
|
||||
4. 光标瞬移 - 在点击前移动。
|
||||
5. 选择下拉菜单显示异常 - 展示移动过程,然后选择选项。
|
||||
6. 模态框显得突兀 - 在确认前添加阅读暂停。
|
||||
7. 视频文件路径随机 - 将其复制到稳定的输出名称。
|
||||
8. 选择器失败被吞没 - 切勿使用静默捕获块。
|
||||
9. 字段类型被假设 - 先探索它们。
|
||||
10. 功能被假设 - 在编写脚本前检查实际 UI。
|
||||
11. 占位符选择值看起来真实 - 注意 `"0"` 和 `"Select..."`。
|
||||
12. 弹出窗口创建单独的视频 - 显式捕获弹出页面,必要时稍后合并。
|
||||
Reference in New Issue
Block a user