docs(zh-CN): sync Chinese docs with latest upstream changes

This commit is contained in:
neo
2026-03-21 12:55:58 +08:00
parent 0af0fbf40b
commit e73c2ffa34
85 changed files with 11028 additions and 747 deletions

View File

@@ -0,0 +1,149 @@
---
name: agent-eval
description: 编码代理Claude Code、Aider、Codex等在自定义任务上的直接比较包含通过率、成本、时间和一致性指标
origin: ECC
tools: Read, Write, Edit, Bash, Grep, Glob
---
# Agent Eval 技能
一个轻量级 CLI 工具,用于在可复现的任务上对编码代理进行头对头比较。每个“哪个编码代理最好?”的比较都基于感觉——本工具将其系统化。
## 何时使用
* 在你自己的代码库上比较编码代理Claude Code、Aider、Codex 等)
* 在采用新工具或模型之前衡量代理性能
* 当代理更新其模型或工具时运行回归检查
* 为团队做出数据支持的代理选择决策
## 安装
```bash
# pinned to v0.1.0 — latest stable commit
pip install git+https://github.com/joaquinhuigomez/agent-eval.git@6d062a2f5cda6ea443bf5d458d361892c04e749b
```
## 核心概念
### YAML 任务定义
以声明方式定义任务。每个任务指定要做什么、要修改哪些文件以及如何判断成功:
```yaml
name: add-retry-logic
description: Add exponential backoff retry to the HTTP client
repo: ./my-project
files:
- src/http_client.py
prompt: |
Add retry logic with exponential backoff to all HTTP requests.
Max 3 retries. Initial delay 1s, max delay 30s.
judge:
- type: pytest
command: pytest tests/test_http_client.py -v
- type: grep
pattern: "exponential_backoff|retry"
files: src/http_client.py
commit: "abc1234" # pin to specific commit for reproducibility
```
### Git 工作树隔离
每个代理运行都获得自己的 git 工作树——无需 Docker。这提供了可复现的隔离使得代理之间不会相互干扰或损坏基础仓库。
### 收集的指标
| 指标 | 衡量内容 |
|--------|-----------------|
| 通过率 | 代理生成的代码是否通过了判断? |
| 成本 | 每个任务的 API 花费(如果可用) |
| 时间 | 完成所需的挂钟秒数 |
| 一致性 | 跨重复运行的通过率例如3/3 = 100% |
## 工作流程
### 1. 定义任务
创建一个 `tasks/` 目录,其中包含 YAML 文件,每个任务一个文件:
```bash
mkdir tasks
# Write task definitions (see template above)
```
### 2. 运行代理
针对你的任务执行代理:
```bash
agent-eval run --task tasks/add-retry-logic.yaml --agent claude-code --agent aider --runs 3
```
每次运行:
1. 从指定的提交创建一个新的 git 工作树
2. 将提示交给代理
3. 运行判断标准
4. 记录通过/失败、成本和时间
### 3. 比较结果
生成比较报告:
```bash
agent-eval report --format table
```
```
Task: add-retry-logic (3 runs each)
┌──────────────┬───────────┬────────┬────────┬─────────────┐
│ Agent │ Pass Rate │ Cost │ Time │ Consistency │
├──────────────┼───────────┼────────┼────────┼─────────────┤
│ claude-code │ 3/3 │ $0.12 │ 45s │ 100% │
│ aider │ 2/3 │ $0.08 │ 38s │ 67% │
└──────────────┴───────────┴────────┴────────┴─────────────┘
```
## 判断类型
### 基于代码(确定性)
```yaml
judge:
- type: pytest
command: pytest tests/ -v
- type: command
command: npm run build
```
### 基于模式
```yaml
judge:
- type: grep
pattern: "class.*Retry"
files: src/**/*.py
```
### 基于模型LLM 作为判断器)
```yaml
judge:
- type: llm
prompt: |
Does this implementation correctly handle exponential backoff?
Check for: max retries, increasing delays, jitter.
```
## 最佳实践
* **从 3-5 个任务开始**,这些任务代表你的真实工作负载,而非玩具示例
* **每个代理至少运行 3 次试验**以捕捉方差——代理是非确定性的
* **在你的任务 YAML 中固定提交**,以便结果在数天/数周内可复现
* **每个任务至少包含一个确定性判断器**测试、构建——LLM 判断器会增加噪音
* **跟踪成本与通过率**——一个通过率 95% 但成本高出 10 倍的代理可能不是正确的选择
* **对你的任务定义进行版本控制**——它们是测试夹具,应将其视为代码
## 链接
* 仓库:[github.com/joaquinhuigomez/agent-eval](https://github.com/joaquinhuigomez/agent-eval)

View File

@@ -0,0 +1,387 @@
---
name: ai-regression-testing
description: AI辅助开发的回归测试策略。沙盒模式API测试无需依赖数据库自动化的缺陷检查工作流程以及捕捉AI盲点的模式其中同一模型编写和审查代码。
origin: ECC
---
# AI 回归测试
专为 AI 辅助开发设计的测试模式,其中同一模型编写代码并审查代码——这会形成系统性的盲点,只有自动化测试才能发现。
## 何时激活
* AI 代理Claude Code、Cursor、Codex已修改 API 路由或后端逻辑
* 发现并修复了一个 bug——需要防止重新引入
* 项目具有沙盒/模拟模式,可用于无需数据库的测试
* 在代码更改后运行 `/bug-check` 或类似的审查命令
* 存在多个代码路径(沙盒与生产环境、功能开关等)
## 核心问题
当 AI 编写代码然后审查其自身工作时,它会将相同的假设带入这两个步骤。这会形成一个可预测的失败模式:
```
AI writes fix → AI reviews fix → AI says "looks correct" → Bug still exists
```
**实际示例**(在生产环境中观察到):
```
Fix 1: Added notification_settings to API response
→ Forgot to add it to the SELECT query
→ AI reviewed and missed it (same blind spot)
Fix 2: Added it to SELECT query
→ TypeScript build error (column not in generated types)
→ AI reviewed Fix 1 but didn't catch the SELECT issue
Fix 3: Changed to SELECT *
→ Fixed production path, forgot sandbox path
→ AI reviewed and missed it AGAIN (4th occurrence)
Fix 4: Test caught it instantly on first run ✅
```
模式:**沙盒/生产环境路径不一致**是 AI 引入的 #1 回归问题。
## 沙盒模式 API 测试
大多数具有 AI 友好架构的项目都有一个沙盒/模拟模式。这是实现快速、无需数据库的 API 测试的关键。
### 设置Vitest + Next.js App Router
```typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
```
```typescript
// __tests__/setup.ts
// Force sandbox mode — no database needed
process.env.SANDBOX_MODE = "true";
process.env.NEXT_PUBLIC_SUPABASE_URL = "";
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "";
```
### Next.js API 路由的测试辅助工具
```typescript
// __tests__/helpers.ts
import { NextRequest } from "next/server";
export function createTestRequest(
url: string,
options?: {
method?: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
sandboxUserId?: string;
},
): NextRequest {
const { method = "GET", body, headers = {}, sandboxUserId } = options || {};
const fullUrl = url.startsWith("http") ? url : `http://localhost:3000${url}`;
const reqHeaders: Record<string, string> = { ...headers };
if (sandboxUserId) {
reqHeaders["x-sandbox-user-id"] = sandboxUserId;
}
const init: { method: string; headers: Record<string, string>; body?: string } = {
method,
headers: reqHeaders,
};
if (body) {
init.body = JSON.stringify(body);
reqHeaders["content-type"] = "application/json";
}
return new NextRequest(fullUrl, init);
}
export async function parseResponse(response: Response) {
const json = await response.json();
return { status: response.status, json };
}
```
### 编写回归测试
关键原则:**为已发现的 bug 编写测试,而不是为正常工作的代码编写测试**。
```typescript
// __tests__/api/user/profile.test.ts
import { describe, it, expect } from "vitest";
import { createTestRequest, parseResponse } from "../../helpers";
import { GET, PATCH } from "@/app/api/user/profile/route";
// Define the contract — what fields MUST be in the response
const REQUIRED_FIELDS = [
"id",
"email",
"full_name",
"phone",
"role",
"created_at",
"avatar_url",
"notification_settings", // ← Added after bug found it missing
];
describe("GET /api/user/profile", () => {
it("returns all required fields", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { status, json } = await parseResponse(res);
expect(status).toBe(200);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
// Regression test — this exact bug was introduced by AI 4 times
it("notification_settings is not undefined (BUG-R1 regression)", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { json } = await parseResponse(res);
expect("notification_settings" in json.data).toBe(true);
const ns = json.data.notification_settings;
expect(ns === null || typeof ns === "object").toBe(true);
});
});
```
### 测试沙盒/生产环境一致性
最常见的 AI 回归问题:修复了生产环境路径但忘记了沙盒路径(或反之)。
```typescript
// Test that sandbox responses match the expected contract
describe("GET /api/user/messages (conversation list)", () => {
it("includes partner_name in sandbox mode", async () => {
const req = createTestRequest("/api/user/messages", {
sandboxUserId: "user-001",
});
const res = await GET(req);
const { json } = await parseResponse(res);
// This caught a bug where partner_name was added
// to production path but not sandbox path
if (json.data.length > 0) {
for (const conv of json.data) {
expect("partner_name" in conv).toBe(true);
}
}
});
});
```
## 将测试集成到 Bug 检查工作流中
### 自定义命令定义
```markdown
<!-- .claude/commands/bug-check.md -->
# Bug 检查
## 步骤 1自动化测试强制不可跳过
在代码审查前**首先**运行以下命令:
npm run test # Vitest 测试套件
npm run build # TypeScript 类型检查 + 构建
- 如果测试失败 → 报告为最高优先级 Bug
- 如果构建失败 → 将类型错误报告为最高优先级
- 只有在两者都通过后,才能继续到步骤 2
## 步骤 2代码审查AI 审查)
1. 沙盒/生产环境路径一致性
2. API 响应结构是否符合前端预期
3. SELECT 子句的完整性
4. 包含回滚的错误处理
5. 乐观更新的竞态条件
## 步骤 3对于每个修复的 Bug提出回归测试方案
```
### 工作流程
```
User: "バグチェックして" (or "/bug-check")
├─ Step 1: npm run test
│ ├─ FAIL → Bug found mechanically (no AI judgment needed)
│ └─ PASS → Continue
├─ Step 2: npm run build
│ ├─ FAIL → Type error found mechanically
│ └─ PASS → Continue
├─ Step 3: AI code review (with known blind spots in mind)
│ └─ Findings reported
└─ Step 4: For each fix, write a regression test
└─ Next bug-check catches if fix breaks
```
## 常见的 AI 回归模式
### 模式 1沙盒/生产环境路径不匹配
**频率**:最常见(在 4 个回归问题中观察到 3 个)
```typescript
// ❌ AI adds field to production path only
if (isSandboxMode()) {
return { data: { id, email, name } }; // Missing new field
}
// Production path
return { data: { id, email, name, notification_settings } };
// ✅ Both paths must return the same shape
if (isSandboxMode()) {
return { data: { id, email, name, notification_settings: null } };
}
return { data: { id, email, name, notification_settings } };
```
**用于捕获它的测试**
```typescript
it("sandbox and production return same fields", async () => {
// In test env, sandbox mode is forced ON
const res = await GET(createTestRequest("/api/user/profile"));
const { json } = await parseResponse(res);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
```
### 模式 2SELECT 子句遗漏
**频率**:在使用 Supabase/Prisma 添加新列时常见
```typescript
// ❌ New column added to response but not to SELECT
const { data } = await supabase
.from("users")
.select("id, email, name") // notification_settings not here
.single();
return { data: { ...data, notification_settings: data.notification_settings } };
// → notification_settings is always undefined
// ✅ Use SELECT * or explicitly include new columns
const { data } = await supabase
.from("users")
.select("*")
.single();
```
### 模式 3错误状态泄漏
**频率**:中等——当向现有组件添加错误处理时
```typescript
// ❌ Error state set but old data not cleared
catch (err) {
setError("Failed to load");
// reservations still shows data from previous tab!
}
// ✅ Clear related state on error
catch (err) {
setReservations([]); // Clear stale data
setError("Failed to load");
}
```
### 模式 4乐观更新未正确回滚
```typescript
// ❌ No rollback on failure
const handleRemove = async (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
await fetch(`/api/items/${id}`, { method: "DELETE" });
// If API fails, item is gone from UI but still in DB
};
// ✅ Capture previous state and rollback on failure
const handleRemove = async (id: string) => {
const prevItems = [...items];
setItems(prev => prev.filter(i => i.id !== id));
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("API error");
} catch {
setItems(prevItems); // Rollback
alert("削除に失敗しました");
}
};
```
## 策略:在发现 Bug 的地方进行测试
不要追求 100% 的覆盖率。相反:
```
Bug found in /api/user/profile → Write test for profile API
Bug found in /api/user/messages → Write test for messages API
Bug found in /api/user/favorites → Write test for favorites API
No bug in /api/user/notifications → Don't write test (yet)
```
**为什么这在 AI 开发中有效:**
1. AI 倾向于重复犯**同一类错误**
2. Bug 集中在复杂区域(身份验证、多路径逻辑、状态管理)
3. 一旦经过测试,该特定回归问题**就不会再次发生**
4. 测试数量随着 Bug 修复而有机增长——没有浪费精力
## 快速参考
| AI 回归模式 | 测试策略 | 优先级 |
|---|---|---|
| 沙盒/生产环境不匹配 | 断言沙盒模式下响应结构相同 | 🔴 高 |
| SELECT 子句遗漏 | 断言响应中包含所有必需字段 | 🔴 高 |
| 错误状态泄漏 | 断言出错时状态已清理 | 🟡 中 |
| 缺少回滚 | 断言 API 失败时状态已恢复 | 🟡 中 |
| 类型转换掩盖 null | 断言字段不为 undefined | 🟡 中 |
## 要 / 不要
**要:**
* 发现 bug 后立即编写测试(如果可能,在修复之前)
* 测试 API 响应结构,而不是实现细节
* 将运行测试作为每次 bug 检查的第一步
* 保持测试快速(在沙盒模式下总计 < 1 秒)
* 以测试所预防的 bug 来命名测试(例如,"BUG-R1 regression"
**不要:**
* 为从未出现过 bug 的代码编写测试
* 相信 AI 自我审查可以作为自动化测试的替代品
* 因为“只是模拟数据”而跳过沙盒路径测试
* 在单元测试足够时编写集成测试
* 追求覆盖率百分比——追求回归预防

View File

@@ -0,0 +1,183 @@
---
name: architecture-decision-records
description: 在Claude Code会话期间将做出的架构决策捕获为结构化的架构决策记录ADR。自动检测决策时刻记录上下文、考虑的替代方案和理由。维护一个ADR日志以便未来的开发人员理解代码库为何以当前方式构建。
origin: ECC
---
# 架构决策记录
在编码会话期间捕捉架构决策。让决策不仅存在于 Slack 线程、PR 评论或某人的记忆中,此技能将生成结构化的 ADR 文档,并与代码并存。
## 何时激活
* 用户明确说"让我们记录这个决定"或"为这个做 ADR"
* 用户在重要的备选方案框架、库、模式、数据库、API 设计)之间做出选择
* 用户说"我们决定..."或"我们选择 X 而不是 Y 的原因是..."
* 用户询问"我们为什么选择了 X"(读取现有 ADR
* 在讨论架构权衡的规划阶段
## ADR 格式
使用 Michael Nygard 提出的轻量级 ADR 格式,并针对 AI 辅助开发进行调整:
```markdown
# ADR-NNNN: [决策标题]
**日期**: YYYY-MM-DD
**状态**: 提议中 | 已接受 | 已弃用 | 被 ADR-NNNN 取代
**决策者**: [相关人员]
## 背景
我们观察到的促使做出此决策或变更的问题是什么?
[用 2-5 句话描述当前情况、约束条件和影响因素]
## 决策
我们提议和/或正在进行的变更是什么?
[用 1-3 句话清晰地陈述决策]
## 考虑的备选方案
### 备选方案 1: [名称]
- **优点**: [益处]
- **缺点**: [弊端]
- **为何不选**: [被拒绝的具体原因]
### 备选方案 2: [名称]
- **优点**: [益处]
- **缺点**: [弊端]
- **为何不选**: [被拒绝的具体原因]
## 影响
由于此变更,哪些事情会变得更容易或更困难?
### 积极影响
- [益处 1]
- [益处 2]
### 消极影响
- [权衡 1]
- [权衡 2]
### 风险
- [风险及缓解措施]
```
## 工作流程
### 捕捉新的 ADR
当检测到决策时刻时:
1. **初始化(仅首次)** — 如果 `docs/adr/` 不存在,在创建目录、一个包含索引表头(见下方 ADR 索引格式)的 `README.md` 以及一个供手动使用的空白 `template.md` 之前,询问用户进行确认。未经明确同意,不要创建文件。
2. **识别决策** — 提取正在做出的核心架构选择
3. **收集上下文** — 是什么问题引发了此决策?存在哪些约束?
4. **记录备选方案** — 考虑了哪些其他选项?为什么拒绝了它们?
5. **陈述后果** — 权衡是什么?什么变得更容易/更难?
6. **分配编号** — 扫描 `docs/adr/` 中的现有 ADR 并递增
7. **确认并写入** — 向用户展示 ADR 草稿以供审查。仅在获得明确批准后写入 `docs/adr/NNNN-decision-title.md`。如果用户拒绝,则丢弃草稿,不写入任何文件。
8. **更新索引** — 追加到 `docs/adr/README.md`
### 读取现有 ADR
当用户询问"我们为什么选择了 X"时:
1. 检查 `docs/adr/` 是否存在 — 如果不存在,回复:"在此项目中未找到 ADR。您想开始记录架构决策吗"
2. 如果存在,扫描 `docs/adr/README.md` 索引以查找相关条目
3. 读取匹配的 ADR 文件并呈现上下文和决策部分
4. 如果未找到匹配项,回复:"未找到关于该决策的 ADR。您现在想记录一个吗"
### ADR 目录结构
```
docs/
└── adr/
├── README.md ← index of all ADRs
├── 0001-use-nextjs.md
├── 0002-postgres-over-mongo.md
├── 0003-rest-over-graphql.md
└── template.md ← blank template for manual use
```
### ADR 索引格式
```markdown
# 架构决策记录
| ADR | 标题 | 状态 | 日期 |
|-----|-------|--------|------|
| [0001](0001-use-nextjs.md) | 使用 Next.js 作为前端框架 | 已采纳 | 2026-01-15 |
| [0002](0002-postgres-over-mongo.md) | 主数据存储选用 PostgreSQL 而非 MongoDB | 已采纳 | 2026-01-20 |
| [0003](0003-rest-over-graphql.md) | 选用 REST API 而非 GraphQL | 已采纳 | 2026-02-01 |
```
## 决策检测信号
留意对话中指示架构决策的以下模式:
**显式信号**
* "让我们选择 X"
* "我们应该使用 X 而不是 Y"
* "权衡是值得的,因为..."
* "将此记录为 ADR"
**隐式信号**(建议记录 ADR — 未经用户确认不要自动创建)
* 比较两个框架或库并得出结论
* 做出数据库模式设计选择并陈述理由
* 在架构模式之间选择(单体 vs 微服务REST vs GraphQL
* 决定身份验证/授权策略
* 评估备选方案后选择部署基础设施
## 优秀 ADR 的要素
### 应该做
* **具体明确** — "使用 Prisma ORM",而不是"使用一个 ORM"
* **记录原因** — 理由比内容更重要
* **包含被拒绝的备选方案** — 未来的开发者需要知道考虑了哪些选项
* **诚实地陈述后果** — 每个决策都有权衡
* **保持简短** — 一份 ADR 应在 2 分钟内可读完
* **使用现在时态** — "我们使用 X",而不是"我们将使用 X"
### 不应该做
* 记录琐碎的决定 — 变量命名或格式化选择不需要 ADR
* 写成论文 — 如果上下文部分超过 10 行,就太长了
* 省略备选方案 — "我们只是选了它"不是一个有效的理由
* 追溯记录而不加标记 — 如果记录过去的决定,请注明原始日期
* 让 ADR 过时 — 被取代的决策应引用其替代品
## ADR 生命周期
```
proposed → accepted → [deprecated | superseded by ADR-NNNN]
```
* **proposed**:决策正在讨论中,尚未确定
* **accepted**:决策已生效并正在遵循
* **deprecated**:决策不再相关(例如,功能已移除)
* **superseded**:更新的 ADR 取代了此决策(始终链接替代品)
## 值得记录的决策类别
| 类别 | 示例 |
|----------|---------|
| **技术选择** | 框架、语言、数据库、云提供商 |
| **架构模式** | 单体 vs 微服务、事件驱动、CQRS |
| **API 设计** | REST vs GraphQL、版本控制策略、认证机制 |
| **数据建模** | 模式设计、规范化决策、缓存策略 |
| **基础设施** | 部署模型、CI/CD 流水线、监控堆栈 |
| **安全** | 认证策略、加密方法、密钥管理 |
| **测试** | 测试框架、覆盖率目标、E2E 与集成测试的平衡 |
| **流程** | 分支策略、评审流程、发布节奏 |
## 与其他技能的集成
* **规划代理**:当规划者提出架构变更时,建议创建 ADR
* **代码审查代理**:标记引入架构变更但未附带相应 ADR 的 PR

View File

@@ -0,0 +1,84 @@
---
name: bun-runtime
description: Bun 作为运行时、包管理器、打包器和测试运行器。何时选择 Bun 而非 Node、迁移注意事项以及 Vercel 支持。
origin: ECC
---
# Bun 运行时
Bun 是一个快速的全能 JavaScript 运行时和工具集:运行时、包管理器、打包器和测试运行器。
## 何时使用
* **优先选择 Bun** 用于:新的 JS/TS 项目、安装/运行速度很重要的脚本、使用 Bun 运行时的 Vercel 部署,以及当您想要单一工具链(运行 + 安装 + 测试 + 构建)时。
* **优先选择 Node** 用于:最大的生态系统兼容性、假定使用 Node 的遗留工具,或者当某个依赖项存在已知的 Bun 问题时。
在以下情况下使用:采用 Bun、从 Node 迁移、编写或调试 Bun 脚本/测试,或在 Vercel 或其他平台上配置 Bun。
## 工作原理
* **运行时**:开箱即用的 Node 兼容运行时(基于 JavaScriptCore用 Zig 实现)。
* **包管理器**`bun install` 比 npm/yarn 快得多。在当前 Bun 中,锁文件默认为 `bun.lock`(文本);旧版本使用 `bun.lockb`(二进制)。
* **打包器**:用于应用程序和库的内置打包器和转译器。
* **测试运行器**:内置的 `bun test`,具有类似 Jest 的 API。
**从 Node 迁移**:将 `node script.js` 替换为 `bun run script.js``bun script.js`。运行 `bun install` 代替 `npm install`;大多数包都能工作。使用 `bun run` 来执行 npm 脚本;使用 `bun x` 进行 npx 风格的临时运行。支持 Node 内置模块;在存在 Bun API 的地方优先使用它们以获得更好的性能。
**Vercel**:在项目设置中将运行时设置为 Bun。构建命令`bun run build``bun build ./src/index.ts --outdir=dist`。安装命令:`bun install --frozen-lockfile` 用于可重复的部署。
## 示例
### 运行和安装
```bash
# Install dependencies (creates/updates bun.lock or bun.lockb)
bun install
# Run a script or file
bun run dev
bun run src/index.ts
bun src/index.ts
```
### 脚本和环境变量
```bash
bun run --env-file=.env dev
FOO=bar bun run script.ts
```
### 测试
```bash
bun test
bun test --watch
```
```typescript
// test/example.test.ts
import { expect, test } from "bun:test";
test("add", () => {
expect(1 + 2).toBe(3);
});
```
### 运行时 API
```typescript
const file = Bun.file("package.json");
const json = await file.json();
Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello");
},
});
```
## 最佳实践
* 提交锁文件(`bun.lock``bun.lockb`)以实现可重复的安装。
* 在脚本中优先使用 `bun run`。对于 TypeScriptBun 原生运行 `.ts`
* 保持依赖项最新Bun 和生态系统发展迅速。

View File

@@ -0,0 +1,104 @@
---
name: claude-devfleet
description: 通过Claude DevFleet协调多智能体编码任务——规划项目、在隔离的工作树中并行调度智能体、监控进度并读取结构化报告。
origin: community
---
# Claude DevFleet 多智能体编排
## 使用时机
当需要调度多个 Claude Code 智能体并行处理编码任务时使用此技能。每个智能体在独立的 git worktree 中运行,并配备全套工具。
需要连接一个通过 MCP 运行的 Claude DevFleet 实例:
```bash
claude mcp add devfleet --transport http http://localhost:18801/mcp
```
## 工作原理
```
User → "Build a REST API with auth and tests"
plan_project(prompt) → project_id + mission DAG
Show plan to user → get approval
dispatch_mission(M1) → Agent 1 spawns in worktree
M1 completes → auto-merge → auto-dispatch M2 (depends_on M1)
M2 completes → auto-merge
get_report(M2) → files_changed, what_done, errors, next_steps
Report back to user
```
### 工具
| 工具 | 用途 |
|------|---------|
| `plan_project(prompt)` | AI 将描述分解为包含链式任务的项目 |
| `create_project(name, path?, description?)` | 手动创建项目,返回 `project_id` |
| `create_mission(project_id, title, prompt, depends_on?, auto_dispatch?)` | 添加任务。`depends_on` 是任务 ID 字符串列表(例如 `["abc-123"]`)。设置 `auto_dispatch=true` 可在依赖满足时自动启动。 |
| `dispatch_mission(mission_id, model?, max_turns?)` | 启动智能体执行任务 |
| `cancel_mission(mission_id)` | 停止正在运行的智能体 |
| `wait_for_mission(mission_id, timeout_seconds?)` | 阻塞直到任务完成(见下方说明) |
| `get_mission_status(mission_id)` | 检查任务进度而不阻塞 |
| `get_report(mission_id)` | 读取结构化报告(更改的文件、测试情况、错误、后续步骤) |
| `get_dashboard()` | 系统概览:运行中的智能体、统计信息、近期活动 |
| `list_projects()` | 浏览所有项目 |
| `list_missions(project_id, status?)` | 列出项目中的任务 |
> **关于 `wait_for_mission` 的说明:** 此操作会阻塞对话,最长 `timeout_seconds` 秒(默认 600 秒)。对于长时间运行的任务,建议改为每 30-60 秒使用 `get_mission_status` 轮询,以便用户能看到进度更新。
### 工作流:规划 → 调度 → 监控 → 报告
1. **规划**:调用 `plan_project(prompt="...")` → 返回 `project_id` 以及带有 `depends_on` 链和 `auto_dispatch=true` 的任务列表。
2. **展示计划**:向用户呈现任务标题、类型和依赖链。
3. **调度**:对根任务(`depends_on` 为空)调用 `dispatch_mission(mission_id=<first_mission_id>)`。剩余任务在其依赖项完成时自动调度(因为 `plan_project` 为它们设置了 `auto_dispatch=true`)。
4. **监控**:调用 `get_mission_status(mission_id=...)``get_dashboard()` 检查进度。
5. **报告**:任务完成后调用 `get_report(mission_id=...)`。与用户分享亮点。
### 并发性
DevFleet 默认最多同时运行 3 个智能体(可通过 `DEVFLEET_MAX_AGENTS` 配置)。当所有槽位都占满时,设置了 `auto_dispatch=true` 的任务会在任务监视器中排队,并在槽位空闲时自动调度。检查 `get_dashboard()` 了解当前槽位使用情况。
## 示例
### 全自动:规划并启动
1. `plan_project(prompt="...")` → 显示包含任务和依赖关系的计划。
2. 调度第一个任务(`depends_on` 为空的那个)。
3. 剩余任务在依赖关系解决时自动调度(它们具有 `auto_dispatch=true`)。
4. 报告项目 ID 和任务数量,让用户知道启动了哪些内容。
5. 定期使用 `get_mission_status``get_dashboard()` 轮询,直到所有任务达到终止状态(`completed``failed``cancelled`)。
6. 对每个终止任务执行 `get_report(mission_id=...)`——总结成功之处,并指出失败任务及其错误和后续步骤。
### 手动:逐步控制
1. `create_project(name="My Project")` → 返回 `project_id`
2. 为第一个(根)任务执行 `create_mission(project_id=project_id, title="...", prompt="...", auto_dispatch=true)` → 捕获 `root_mission_id`
为每个后续任务执行 `create_mission(project_id=project_id, title="...", prompt="...", auto_dispatch=true, depends_on=["<root_mission_id>"])`
3. 在第一个任务上执行 `dispatch_mission(mission_id=...)` 以启动链。
4. 完成后执行 `get_report(mission_id=...)`
### 带审查的串行执行
1. `create_project(name="...")` → 获取 `project_id`
2. `create_mission(project_id=project_id, title="Implement feature", prompt="...")` → 获取 `impl_mission_id`
3. `dispatch_mission(mission_id=impl_mission_id)`,然后使用 `get_mission_status` 轮询直到完成。
4. `get_report(mission_id=impl_mission_id)` 以审查结果。
5. `create_mission(project_id=project_id, title="Review", prompt="...", depends_on=[impl_mission_id], auto_dispatch=true)` —— 由于依赖已满足,自动启动。
## 指南
* 在调度前始终与用户确认计划,除非用户已明确指示继续。
* 报告状态时包含任务标题和 ID。
* 如果任务失败,在重试前读取其报告。
* 批量调度前检查 `get_dashboard()` 了解智能体槽位可用性。
* 任务依赖关系构成一个有向无环图DAG——不要创建循环依赖。
* 每个智能体在独立的 git worktree 中运行,并在完成时自动合并。如果发生合并冲突,更改将保留在智能体的 worktree 分支上,以便手动解决。
* 手动创建任务时,如果希望它们在依赖项完成时自动触发,请始终设置 `auto_dispatch=true`。没有此标志,任务将保持 `draft` 状态。

View File

@@ -0,0 +1,243 @@
---
name: codebase-onboarding
description: 分析一个陌生的代码库并生成一个结构化的入门指南包括架构图、关键入口点、规范和一个起始的CLAUDE.md文件。适用于加入新项目或首次在代码仓库中设置Claude Code时。
origin: ECC
---
# 代码库入门引导
系统性地分析一个不熟悉的代码库,并生成结构化的入门指南。专为加入新项目的开发者或首次在现有仓库中设置 Claude Code 的用户设计。
## 使用时机
* 首次使用 Claude Code 打开项目时
* 加入新团队或新仓库时
* 用户询问“帮我理解这个代码库”
* 用户要求为项目生成 CLAUDE.md 文件
* 用户说“带我入门”或“带我浏览这个仓库”
## 工作原理
### 阶段 1初步侦察
在不阅读每个文件的情况下,收集关于项目的原始信息。并行运行以下检查:
```
1. Package manifest detection
→ package.json, go.mod, Cargo.toml, pyproject.toml, pom.xml, build.gradle,
Gemfile, composer.json, mix.exs, pubspec.yaml
2. Framework fingerprinting
→ next.config.*, nuxt.config.*, angular.json, vite.config.*,
django settings, flask app factory, fastapi main, rails config
3. Entry point identification
→ main.*, index.*, app.*, server.*, cmd/, src/main/
4. Directory structure snapshot
→ Top 2 levels of the directory tree, ignoring node_modules, vendor,
.git, dist, build, __pycache__, .next
5. Config and tooling detection
→ .eslintrc*, .prettierrc*, tsconfig.json, Makefile, Dockerfile,
docker-compose*, .github/workflows/, .env.example, CI configs
6. Test structure detection
→ tests/, test/, __tests__/, *_test.go, *.spec.ts, *.test.js,
pytest.ini, jest.config.*, vitest.config.*
```
### 阶段 2架构映射
根据侦察数据,识别:
**技术栈**
* 语言及版本限制
* 框架及主要库
* 数据库及 ORM
* 构建工具和打包器
* CI/CD 平台
**架构模式**
* 单体、单体仓库、微服务,还是无服务器
* 前端/后端分离,还是全栈
* API 风格REST、GraphQL、gRPC、tRPC
**关键目录**
将顶级目录映射到其用途:
<!-- Example for a React project — replace with detected directories -->
```
src/components/ → React UI components
src/api/ → API route handlers
src/lib/ → Shared utilities
src/db/ → Database models and migrations
tests/ → Test suites
scripts/ → Build and deployment scripts
```
**数据流**
追踪一个请求从入口到响应的路径:
* 请求从哪里进入?(路由器、处理器、控制器)
* 如何进行验证?(中间件、模式、守卫)
* 业务逻辑在哪里?(服务、模型、用例)
* 如何访问数据库ORM、原始查询、存储库
### 阶段 3规范检测
识别代码库已遵循的模式:
**命名规范**
* 文件命名kebab-case、camelCase、PascalCase、snake\_case
* 组件/类命名模式
* 测试文件命名:`*.test.ts``*.spec.ts``*_test.go`
**代码模式**
* 错误处理风格try/catch、Result 类型、错误码
* 依赖注入还是直接导入
* 状态管理方法
* 异步模式回调、Promise、async/await、通道
**Git 规范**
* 根据最近分支推断分支命名
* 根据最近提交推断提交信息风格
* PR 工作流(压缩合并、合并、变基)
* 如果仓库尚无提交记录或历史记录很浅(例如 `git clone --depth 1`则跳过此部分并注明“Git 历史记录不可用或过浅,无法检测规范”
### 阶段 4生成入门工件
生成两个输出:
#### 输出 1入门指南
```markdown
# 新手上路指南:[项目名称]
## 概述
[2-3句话说明本项目的作用及服务对象]
## 技术栈
<!-- Example for a Next.js project — replace with detected stack -->
| 层级 | 技术 | 版本 |
|-------|-----------|---------|
| 语言 | TypeScript | 5.x |
| 框架 | Next.js | 14.x |
| 数据库 | PostgreSQL | 16 |
| ORM | Prisma | 5.x |
| 测试 | Jest + Playwright | - |
## 架构
[组件连接方式的图表或描述]
## 关键入口点
<!-- Example for a Next.js project — replace with detected paths -->
- **API 路由**: `src/app/api/` — Next.js 路由处理器
- **UI 页面**: `src/app/(dashboard)/` — 经过身份验证的页面
- **数据库**: `prisma/schema.prisma` — 数据模型的单一事实来源
- **配置**: `next.config.ts` — 构建和运行时配置
## 目录结构
[顶级目录 → 用途映射]
## 请求生命周期
[追踪一个 API 请求从入口到响应的全过程]
## 约定
- [文件命名模式]
- [错误处理方法]
- [测试模式]
- [Git 工作流程]
## 常见任务
<!-- Example for a Node.js project — replace with detected commands -->
- **运行开发服务器**: `npm run dev`
- **运行测试**: `npm test`
- **运行代码检查工具**: `npm run lint`
- **数据库迁移**: `npx prisma migrate dev`
- **生产环境构建**: `npm run build`
## 查找位置
<!-- Example for a Next.js project — replace with detected paths -->
| 我想... | 查看... |
|--------------|-----------|
| 添加 API 端点 | `src/app/api/` |
| 添加 UI 页面 | `src/app/(dashboard)/` |
| 添加数据库表 | `prisma/schema.prisma` |
| 添加测试 | `tests/` (与源路径匹配) |
| 更改构建配置 | `next.config.ts` |
```
#### 输出 2初始 CLAUDE.md
根据检测到的规范,生成或更新项目特定的 CLAUDE.md。如果 `CLAUDE.md` 已存在,请先读取它并进行增强——保留现有的项目特定说明,并明确标注新增或更改的内容。
```markdown
# 项目说明
## 技术栈
[检测到的技术栈摘要]
## 代码风格
- [检测到的命名规范]
- [检测到的应遵循的模式]
## 测试
- 运行测试:`[detected test command]`
- 测试模式:[检测到的测试文件约定]
- 覆盖率:[如果已配置,覆盖率命令]
## 构建与运行
- 开发:`[detected dev command]`
- 构建:`[detected build command]`
- 代码检查:`[detected lint command]`
## 项目结构
[关键目录 → 用途映射]
## 约定
- [可检测到的提交风格]
- [可检测到的 PR 工作流程]
- [错误处理模式]
```
## 最佳实践
1. **不要通读所有内容** —— 侦察阶段应使用 Glob 和 Grep而非读取每个文件。仅在信号不明确时有选择性地读取。
2. **验证而非猜测** —— 如果从配置文件中检测到某个框架,但实际代码使用了不同的东西,请以代码为准。
3. **尊重现有的 CLAUDE.md** —— 如果文件已存在,请增强它而不是替换它。明确标注哪些是新增内容,哪些是原有内容。
4. **保持简洁** —— 入门指南应在 2 分钟内可快速浏览。细节应留在代码中,而非指南里。
5. **标记未知项** —— 如果无法自信地检测到某个规范,请如实说明而非猜测。“无法确定测试运行器”比给出错误答案更好。
## 应避免的反模式
* 生成超过 100 行的 CLAUDE.md —— 保持其聚焦
* 列出每个依赖项 —— 仅突出那些影响编码方式的依赖
* 描述显而易见的目录名 —— `src/` 不需要解释
* 复制 README —— 入门指南应提供 README 所缺乏的结构性见解
## 示例
### 示例 1首次进入新仓库
**用户**:“带我入门这个代码库”
**操作**:运行完整的 4 阶段工作流 → 生成入门指南 + 初始 CLAUDE.md
**输出**:入门指南直接打印到对话中,并在项目根目录写入一个 `CLAUDE.md`
### 示例 2为现有项目生成 CLAUDE.md
**用户**:“为这个项目生成一个 CLAUDE.md”
**操作**:运行阶段 1-3跳过入门指南仅生成 CLAUDE.md
**输出**:包含检测到的规范的项目特定 `CLAUDE.md`
### 示例 3增强现有的 CLAUDE.md
**用户**:“用当前项目规范更新 CLAUDE.md”
**操作**:读取现有 CLAUDE.md运行阶段 1-3合并新发现
**输出**:更新后的 `CLAUDE.md`,并明确标记了新增内容

View File

@@ -86,15 +86,14 @@ Default: Core only
### 2b: 选择技能类别
共有41项技能分为8个类别。使用 `AskUserQuestion` 配合 `multiSelect: true`
下方有7个可选的类别组。后续的详细确认列表涵盖了8个类别中的45项技能外加1个独立模板。使用 `AskUserQuestion` `multiSelect: true`
```
Question: "Which skill categories do you want to install?"
Options:
- "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
- "Framework & Language" — "Django, Laravel, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
- "Business & Content" — "Article writing, content engine, market research, investor materials, outreach"
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
- "Social & Content Distribution" — "X/Twitter API, crossposting alongside content-engine"
- "Media Generation" — "fal.ai image/video/audio alongside VideoDB"
@@ -106,25 +105,29 @@ Options:
对于每个选定的类别,打印下面的完整技能列表,并要求用户确认或取消选择特定的技能。如果列表超过 4 项,将列表打印为文本,并使用 `AskUserQuestion`,提供一个 "安装所有列出项" 的选项,以及一个 "其他" 选项供用户粘贴特定名称。
**类别框架与语言17 项技能)**
**类别:框架与语言(21项技能**
| 技能 | 描述 |
|-------|-------------|
| `backend-patterns` | Node.js/Express/Next.js 的后端架构、API 设计、服务器端最佳实践 |
| `coding-standards` | TypeScript、JavaScript、React、Node.js 的通用编码标准 |
| `django-patterns` | Django 架构、使用 DRF 的 REST API、ORM、缓存、信号、中间件 |
| `django-security` | Django 安全性:身份验证、CSRF、SQL 注入、XSS 防护 |
| `django-security` | Django 安全性:证、CSRF、SQL 注入、XSS 防护 |
| `django-tdd` | 使用 pytest-django、factory\_boy、模拟、覆盖率进行 Django 测试 |
| `django-verification` | Django 验证循环:迁移、代码检查、测试、安全扫描 |
| `laravel-patterns` | Laravel 架构模式路由、控制器、Eloquent、队列、缓存 |
| `laravel-security` | Laravel 安全性认证、策略、CSRF、批量赋值、速率限制 |
| `laravel-tdd` | 使用 PHPUnit 和 Pest、工厂、假对象、覆盖率进行 Laravel 测试 |
| `laravel-verification` | Laravel 验证:代码检查、静态分析、测试、安全扫描 |
| `frontend-patterns` | React、Next.js、状态管理、性能、UI 模式 |
| `frontend-slides` | 零依赖的 HTML 演示文稿、样式预览以及 PPTX 到网页的转换 |
| `golang-patterns` | 地道的 Go 模式、构建健 Go 应用程序的约定 |
| `golang-patterns` | 地道的 Go 模式、构建健 Go 应用程序的约定 |
| `golang-testing` | Go 测试:表驱动测试、子测试、基准测试、模糊测试 |
| `java-coding-standards` | Spring Boot 的 Java 编码标准命名、不可变性、Optional、流 |
| `python-patterns` | Pythonic 惯用法、PEP 8、类型提示、最佳实践 |
| `python-testing` | 使用 pytest、TDD、固件、模拟、参数化进行 Python 测试 |
| `springboot-patterns` | Spring Boot 架构、REST API、分层服务、缓存、异步 |
| `springboot-security` | Spring Security身份验证/授权、验证、CSRF、密钥、速率限制 |
| `python-testing` | 使用 pytest、TDD、夹具、模拟、参数化进行 Python 测试 |
| `springboot-patterns` | Spring Boot 架构、REST API、分层服务、缓存、异步处理 |
| `springboot-security` | Spring Security证/授权、验证、CSRF、密钥、速率限制 |
| `springboot-tdd` | 使用 JUnit 5、Mockito、MockMvc、Testcontainers 进行 Spring Boot TDD |
| `springboot-verification` | Spring Boot 验证:构建、静态分析、测试、安全扫描 |
@@ -269,16 +272,17 @@ grep -rn "skills/" $TARGET/skills/
有些技能会引用其他技能。验证这些依赖关系:
* `django-tdd` 可能引用 `django-patterns`
* `springboot-tdd` 可能引用 `springboot-patterns`
* `django-tdd` 可能引用 `django-patterns`
* `laravel-tdd` 可能引用 `laravel-patterns`
* `springboot-tdd` 可能会引用 `springboot-patterns`
* `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录
* `python-testing` 可能引用 `python-patterns`
* `golang-testing` 可能引用 `golang-patterns`
* `python-testing` 可能引用 `python-patterns`
* `golang-testing` 可能引用 `golang-patterns`
* `crosspost` 引用 `content-engine``x-api`
* `deep-research` 引用 `exa-search`补的 MCP 工具)
* `fal-ai-media` 引用 `videodb`补的媒体技能)
* `deep-research` 引用 `exa-search`(补的 MCP 工具)
* `fal-ai-media` 引用 `videodb`(补的媒体技能)
* `x-api` 引用 `content-engine``crosspost`
* 语言特定规则引用 `common/` 对应
* 特定语言的规则引用 `common/` 对应内容
### 4d报告问题

View File

@@ -0,0 +1,143 @@
---
name: context-budget
description: 审核Claude Code上下文窗口在代理、技能、MCP服务器和规则中的消耗情况。识别膨胀、冗余组件并提供优先的令牌节省建议。
origin: ECC
---
# 上下文预算
分析 Claude Code 会话中每个已加载组件的令牌开销,并提供可操作的优化建议以回收上下文空间。
## 使用时机
* 会话性能感觉迟缓或输出质量下降
* 你最近添加了许多技能、代理或 MCP 服务器
* 你想知道实际有多少上下文余量
* 计划添加更多组件,需要知道是否有空间
* 运行 `/context-budget` 命令(本技能为其提供支持)
## 工作原理
### 阶段 1清单
扫描所有组件目录并估算令牌消耗:
**代理** (`agents/*.md`)
* 统计每个文件的行数和令牌数(单词数 × 1.3
* 提取 `description` 前言长度
* 标记:文件 >200 行(繁重),描述 >30 词(臃肿的前言)
**技能** (`skills/*/SKILL.md`)
* 统计 SKILL.md 的令牌数
* 标记:文件 >400 行
* 检查 `.agents/skills/` 中的重复副本 — 跳过相同副本以避免重复计数
**规则** (`rules/**/*.md`)
* 统计每个文件的令牌数
* 标记:文件 >100 行
* 检测同一语言模块中规则文件之间的内容重叠
**MCP 服务器** (`.mcp.json` 或活动的 MCP 配置)
* 统计配置的服务器数量和工具总数
* 估算模式开销约为每个工具 500 令牌
* 标记:工具数 >20 的服务器,包装简单 CLI 命令的服务器 (`gh`, `git`, `npm`, `supabase`, `vercel`)
**CLAUDE.md**(项目级 + 用户级)
* 统计 CLAUDE.md 链中每个文件的令牌数
* 标记:合并总数 >300 行
### 阶段 2分类
将每个组件归入一个类别:
| 类别 | 标准 | 操作 |
|--------|----------|--------|
| **始终需要** | 在 CLAUDE.md 中被引用,支持活动命令,或匹配当前项目类型 | 保留 |
| **有时需要** | 特定领域(例如语言模式),未在 CLAUDE.md 中引用 | 考虑按需激活 |
| **很少需要** | 无命令引用,内容重叠,或无明显的项目匹配 | 移除或延迟加载 |
### 阶段 3检测问题
识别以下问题模式:
* **臃肿的代理描述** — 前言中描述 >30 词,会在每次任务工具调用时加载
* **繁重的代理** — 文件 >200 行,每次生成时都会增加任务工具的上下文
* **冗余组件** — 重复代理逻辑的技能,重复 CLAUDE.md 的规则
* **MCP 超额订阅** — >10 个服务器,或包装了可免费使用的 CLI 工具的服务器
* **CLAUDE.md 臃肿** — 冗长的解释、过时的部分、本应成为规则的指令
### 阶段 4报告
生成上下文预算报告:
```
Context Budget Report
═══════════════════════════════════════
Total estimated overhead: ~XX,XXX tokens
Context model: Claude Sonnet (200K window)
Effective available context: ~XXX,XXX tokens (XX%)
Component Breakdown:
┌─────────────────┬────────┬───────────┐
│ Component │ Count │ Tokens │
├─────────────────┼────────┼───────────┤
│ Agents │ N │ ~X,XXX │
│ Skills │ N │ ~X,XXX │
│ Rules │ N │ ~X,XXX │
│ MCP tools │ N │ ~XX,XXX │
│ CLAUDE.md │ N │ ~X,XXX │
└─────────────────┴────────┴───────────┘
⚠ Issues Found (N):
[ranked by token savings]
Top 3 Optimizations:
1. [action] → save ~X,XXX tokens
2. [action] → save ~X,XXX tokens
3. [action] → save ~X,XXX tokens
Potential savings: ~XX,XXX tokens (XX% of current overhead)
```
在详细模式下,额外输出每个文件的令牌计数、最繁重文件的行级细分、重叠组件之间的具体冗余行,以及 MCP 工具列表和每个工具模式大小的估算。
## 示例
**基本审计**
```
User: /context-budget
Skill: Scans setup → 16 agents (12,400 tokens), 28 skills (6,200), 87 MCP tools (43,500), 2 CLAUDE.md (1,200)
Flags: 3 heavy agents, 14 MCP servers (3 CLI-replaceable)
Top saving: remove 3 MCP servers → -27,500 tokens (47% overhead reduction)
```
**详细模式**
```
User: /context-budget --verbose
Skill: Full report + per-file breakdown showing planner.md (213 lines, 1,840 tokens),
MCP tool list with per-tool sizes, duplicated rule lines side by side
```
**扩容前检查**
```
User: I want to add 5 more MCP servers, do I have room?
Skill: Current overhead 33% → adding 5 servers (~50 tools) would add ~25,000 tokens → pushes to 45% overhead
Recommendation: remove 2 CLI-replaceable servers first to stay under 40%
```
## 最佳实践
* **令牌估算**:对散文使用 `words × 1.3`,对代码密集型文件使用 `chars / 4`
* **MCP 是最大的杠杆**:每个工具模式约消耗 500 令牌;一个 30 个工具的服务器开销超过你所有技能的总和
* **代理描述始终加载**:即使代理从未被调用,其描述字段也存在于每个任务工具上下文中
* **详细模式用于调试**:需要精确定位导致开销的确切文件时使用,而非用于常规审计
* **变更后审计**:添加任何代理、技能或 MCP 服务器后运行,以便及早发现增量

View File

@@ -8,16 +8,14 @@ origin: ECC
将内容分发到多个社交平台,并适配各平台原生风格。
## 何时使用
## 何时激活
* 用户希望将内容发布到多个平台
* 在社交媒体上发布公告、产品发布或更新
* 将某个平台的内容改编后发布到其他平台
* 用户提及“跨平台发布”、“到处发帖”、“分享到所有平台”或“分发这个”
## 运作方式
### 核心规则
## 核心规则
1. **切勿在不同平台发布相同内容。** 每个平台都应获得原生适配版本。
2. **主平台优先。** 先发布到主平台,再为其他平台适配。
@@ -25,7 +23,7 @@ origin: ECC
4. **每条帖子一个核心思想。** 如果源内容包含多个想法,请拆分成多条帖子。
5. **注明出处很重要。** 如果转发他人的内容,请注明来源。
### 平台规
## 平台规
| 平台 | 最大长度 | 链接处理 | 话题标签 | 媒体 |
|----------|-----------|---------------|----------|-------|
@@ -34,7 +32,7 @@ origin: ECC
| Threads | 500 字符 | 独立的链接附件 | 通常不使用 | 图片、视频 |
| Bluesky | 300 字符 | 通过 Facets (富文本) | 无 (使用 Feeds) | 图片 |
### 工作流程
## 工作流程
### 步骤 1创建源内容
@@ -97,7 +95,7 @@ origin: ECC
* 错开发布时间 (不要同时发布 — 间隔 30-60 分钟)
* 在适当的地方包含跨平台引用 (例如,“在 X 上有更长的 Thread”等)
## 示例
## 内容适配示例
### 源内容:产品发布
@@ -178,7 +176,7 @@ resp = requests.post(
"threads": {"text": threads_version}
}
},
timeout=30
timeout=30,
)
resp.raise_for_status()
```

View File

@@ -0,0 +1,772 @@
---
name: data-scraper-agent
description: 构建一个全自动化的AI驱动数据收集代理适用于任何公共来源——招聘网站、价格信息、新闻、GitHub、体育赛事等任何内容。按计划进行抓取使用免费LLMGemini Flash丰富数据将结果存储在Notion/Sheets/Supabase中并从用户反馈中学习。完全免费在GitHub Actions上运行。适用于用户希望自动监控、收集或跟踪任何公共数据的场景。
origin: community
---
# 数据抓取代理
构建一个生产就绪、AI驱动的数据收集代理适用于任何公共数据源。
按计划运行使用免费LLM丰富结果存储到数据库并随时间推移不断改进。
**技术栈Python · Gemini Flash (免费) · GitHub Actions (免费) · Notion / Sheets / Supabase**
## 何时激活
* 用户想要抓取或监控任何公共网站或API
* 用户说"构建一个检查...的机器人"、"为我监控X"、"从...收集数据"
* 用户想要跟踪工作、价格、新闻、仓库、体育比分、事件、列表
* 用户询问如何自动化数据收集而无需支付托管费用
* 用户想要一个能根据他们的决策随时间推移变得更智能的代理
## 核心概念
### 三层架构
每个数据抓取代理都有三层:
```
COLLECT → ENRICH → STORE
│ │ │
Scraper AI (LLM) Database
runs on scores/ Notion /
schedule summarises Sheets /
& classifies Supabase
```
### 免费技术栈
| 层级 | 工具 | 原因 |
|---|---|---|
| **抓取** | `requests` + `BeautifulSoup` | 无成本覆盖80%的公共网站 |
| **JS渲染的网站** | `playwright` (免费) | 当HTML抓取失败时使用 |
| **AI丰富** | 通过REST API的Gemini Flash | 500次请求/天100万令牌/天 — 免费 |
| **存储** | Notion API | 免费层级用于审查的优秀UI |
| **调度** | GitHub Actions cron | 对公共仓库免费 |
| **学习** | 仓库中的JSON反馈文件 | 零基础设施在git中持久化 |
### AI模型后备链
构建代理以在配额耗尽时自动在Gemini模型间回退
```
gemini-2.0-flash-lite (30 RPM) →
gemini-2.0-flash (15 RPM) →
gemini-2.5-flash (10 RPM) →
gemini-flash-lite-latest (fallback)
```
### 批量API调用以提高效率
切勿为每个项目单独调用LLM。始终批量处理
```python
# BAD: 33 API calls for 33 items
for item in items:
result = call_ai(item) # 33 calls → hits rate limit
# GOOD: 7 API calls for 33 items (batch size 5)
for batch in chunks(items, size=5):
results = call_ai(batch) # 7 calls → stays within free tier
```
***
## 工作流程
### 步骤 1: 理解目标
询问用户:
1. **收集什么:** "数据源是什么URL / API / RSS / 公共端点?"
2. **提取什么:** "哪些字段重要标题、价格、URL、日期、分数"
3. **如何存储:** "结果应该存储在哪里Notion、Google Sheets、Supabase还是本地文件"
4. **如何丰富:** "您希望AI对每个项目进行评分、总结、分类或匹配吗"
5. **频率:** "应该多久运行一次?每小时、每天、每周?"
常见的提示示例:
* 招聘网站 → 根据简历评分相关性
* 产品价格 → 降价时发出警报
* GitHub仓库 → 总结新版本
* 新闻源 → 按主题+情感分类
* 体育结果 → 提取统计数据到跟踪器
* 活动日历 → 按兴趣筛选
***
### 步骤 2: 设计代理架构
为用户生成以下目录结构:
```
my-agent/
├── config.yaml # User customises this (keywords, filters, preferences)
├── profile/
│ └── context.md # User context the AI uses (resume, interests, criteria)
├── scraper/
│ ├── __init__.py
│ ├── main.py # Orchestrator: scrape → enrich → store
│ ├── filters.py # Rule-based pre-filter (fast, before AI)
│ └── sources/
│ ├── __init__.py
│ └── source_name.py # One file per data source
├── ai/
│ ├── __init__.py
│ ├── client.py # Gemini REST client with model fallback
│ ├── pipeline.py # Batch AI analysis
│ ├── jd_fetcher.py # Fetch full content from URLs (optional)
│ └── memory.py # Learn from user feedback
├── storage/
│ ├── __init__.py
│ └── notion_sync.py # Or sheets_sync.py / supabase_sync.py
├── data/
│ └── feedback.json # User decision history (auto-updated)
├── .env.example
├── setup.py # One-time DB/schema creation
├── enrich_existing.py # Backfill AI scores on old rows
├── requirements.txt
└── .github/
└── workflows/
└── scraper.yml # GitHub Actions schedule
```
***
### 步骤 3: 构建抓取器源
适用于任何数据源的模板:
```python
# scraper/sources/my_source.py
"""
[Source Name] — scrapes [what] from [where].
Method: [REST API / HTML scraping / RSS feed]
"""
import requests
from bs4 import BeautifulSoup
from datetime import datetime, timezone
from scraper.filters import is_relevant
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)",
}
def fetch() -> list[dict]:
"""
Returns a list of items with consistent schema.
Each item must have at minimum: name, url, date_found.
"""
results = []
# ---- REST API source ----
resp = requests.get("https://api.example.com/items", headers=HEADERS, timeout=15)
if resp.status_code == 200:
for item in resp.json().get("results", []):
if not is_relevant(item.get("title", "")):
continue
results.append(_normalise(item))
return results
def _normalise(raw: dict) -> dict:
"""Convert raw API/HTML data to the standard schema."""
return {
"name": raw.get("title", ""),
"url": raw.get("link", ""),
"source": "MySource",
"date_found": datetime.now(timezone.utc).date().isoformat(),
# add domain-specific fields here
}
```
**HTML抓取模式**
```python
soup = BeautifulSoup(resp.text, "lxml")
for card in soup.select("[class*='listing']"):
title = card.select_one("h2, h3").get_text(strip=True)
link = card.select_one("a")["href"]
if not link.startswith("http"):
link = f"https://example.com{link}"
```
**RSS源模式**
```python
import xml.etree.ElementTree as ET
root = ET.fromstring(resp.text)
for item in root.findall(".//item"):
title = item.findtext("title", "")
link = item.findtext("link", "")
```
***
### 步骤 4: 构建Gemini AI客户端
````python
# ai/client.py
import os, json, time, requests
_last_call = 0.0
MODEL_FALLBACK = [
"gemini-2.0-flash-lite",
"gemini-2.0-flash",
"gemini-2.5-flash",
"gemini-flash-lite-latest",
]
def generate(prompt: str, model: str = "", rate_limit: float = 7.0) -> dict:
"""Call Gemini with auto-fallback on 429. Returns parsed JSON or {}."""
global _last_call
api_key = os.environ.get("GEMINI_API_KEY", "")
if not api_key:
return {}
elapsed = time.time() - _last_call
if elapsed < rate_limit:
time.sleep(rate_limit - elapsed)
models = [model] + [m for m in MODEL_FALLBACK if m != model] if model else MODEL_FALLBACK
_last_call = time.time()
for m in models:
url = f"https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent?key={api_key}"
payload = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
"responseMimeType": "application/json",
"temperature": 0.3,
"maxOutputTokens": 2048,
},
}
try:
resp = requests.post(url, json=payload, timeout=30)
if resp.status_code == 200:
return _parse(resp)
if resp.status_code in (429, 404):
time.sleep(1)
continue
return {}
except requests.RequestException:
return {}
return {}
def _parse(resp) -> dict:
try:
text = (
resp.json()
.get("candidates", [{}])[0]
.get("content", {})
.get("parts", [{}])[0]
.get("text", "")
.strip()
)
if text.startswith("```"):
text = text.split("\n", 1)[-1].rsplit("```", 1)[0]
return json.loads(text)
except (json.JSONDecodeError, KeyError):
return {}
````
***
### 步骤 5: 构建AI管道批量
```python
# ai/pipeline.py
import json
import yaml
from pathlib import Path
from ai.client import generate
def analyse_batch(items: list[dict], context: str = "", preference_prompt: str = "") -> list[dict]:
"""Analyse items in batches. Returns items enriched with AI fields."""
config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
model = config.get("ai", {}).get("model", "gemini-2.5-flash")
rate_limit = config.get("ai", {}).get("rate_limit_seconds", 7.0)
min_score = config.get("ai", {}).get("min_score", 0)
batch_size = config.get("ai", {}).get("batch_size", 5)
batches = [items[i:i + batch_size] for i in range(0, len(items), batch_size)]
print(f" [AI] {len(items)} items → {len(batches)} API calls")
enriched = []
for i, batch in enumerate(batches):
print(f" [AI] Batch {i + 1}/{len(batches)}...")
prompt = _build_prompt(batch, context, preference_prompt, config)
result = generate(prompt, model=model, rate_limit=rate_limit)
analyses = result.get("analyses", [])
for j, item in enumerate(batch):
ai = analyses[j] if j < len(analyses) else {}
if ai:
score = max(0, min(100, int(ai.get("score", 0))))
if min_score and score < min_score:
continue
enriched.append({**item, "ai_score": score, "ai_summary": ai.get("summary", ""), "ai_notes": ai.get("notes", "")})
else:
enriched.append(item)
return enriched
def _build_prompt(batch, context, preference_prompt, config):
priorities = config.get("priorities", [])
items_text = "\n\n".join(
f"Item {i+1}: {json.dumps({k: v for k, v in item.items() if not k.startswith('_')})}"
for i, item in enumerate(batch)
)
return f"""Analyse these {len(batch)} items and return a JSON object.
# Items
{items_text}
# User Context
{context[:800] if context else "Not provided"}
# User Priorities
{chr(10).join(f"- {p}" for p in priorities)}
{preference_prompt}
# Instructions
Return: {{"analyses": [{{"score": <0-100>, "summary": "<2 sentences>", "notes": "<why this matches or doesn't>"}} for each item in order]}}
Be concise. Score 90+=excellent match, 70-89=good, 50-69=ok, <50=weak."""
```
***
### 步骤 6: 构建反馈学习系统
```python
# ai/memory.py
"""Learn from user decisions to improve future scoring."""
import json
from pathlib import Path
FEEDBACK_PATH = Path(__file__).parent.parent / "data" / "feedback.json"
def load_feedback() -> dict:
if FEEDBACK_PATH.exists():
try:
return json.loads(FEEDBACK_PATH.read_text())
except (json.JSONDecodeError, OSError):
pass
return {"positive": [], "negative": []}
def save_feedback(fb: dict):
FEEDBACK_PATH.parent.mkdir(parents=True, exist_ok=True)
FEEDBACK_PATH.write_text(json.dumps(fb, indent=2))
def build_preference_prompt(feedback: dict, max_examples: int = 15) -> str:
"""Convert feedback history into a prompt bias section."""
lines = []
if feedback.get("positive"):
lines.append("# Items the user LIKED (positive signal):")
for e in feedback["positive"][-max_examples:]:
lines.append(f"- {e}")
if feedback.get("negative"):
lines.append("\n# Items the user SKIPPED/REJECTED (negative signal):")
for e in feedback["negative"][-max_examples:]:
lines.append(f"- {e}")
if lines:
lines.append("\nUse these patterns to bias scoring on new items.")
return "\n".join(lines)
```
**与存储层集成:** 每次运行后,从数据库中查询具有正面/负面状态的项,并使用提取的模式调用 `save_feedback()`。
***
### 步骤 7: 构建存储Notion示例
```python
# storage/notion_sync.py
import os
from notion_client import Client
from notion_client.errors import APIResponseError
_client = None
def get_client():
global _client
if _client is None:
_client = Client(auth=os.environ["NOTION_TOKEN"])
return _client
def get_existing_urls(db_id: str) -> set[str]:
"""Fetch all URLs already stored — used for deduplication."""
client, seen, cursor = get_client(), set(), None
while True:
resp = client.databases.query(database_id=db_id, page_size=100, **{"start_cursor": cursor} if cursor else {})
for page in resp["results"]:
url = page["properties"].get("URL", {}).get("url", "")
if url: seen.add(url)
if not resp["has_more"]: break
cursor = resp["next_cursor"]
return seen
def push_item(db_id: str, item: dict) -> bool:
"""Push one item to Notion. Returns True on success."""
props = {
"Name": {"title": [{"text": {"content": item.get("name", "")[:100]}}]},
"URL": {"url": item.get("url")},
"Source": {"select": {"name": item.get("source", "Unknown")}},
"Date Found": {"date": {"start": item.get("date_found")}},
"Status": {"select": {"name": "New"}},
}
# AI fields
if item.get("ai_score") is not None:
props["AI Score"] = {"number": item["ai_score"]}
if item.get("ai_summary"):
props["Summary"] = {"rich_text": [{"text": {"content": item["ai_summary"][:2000]}}]}
if item.get("ai_notes"):
props["Notes"] = {"rich_text": [{"text": {"content": item["ai_notes"][:2000]}}]}
try:
get_client().pages.create(parent={"database_id": db_id}, properties=props)
return True
except APIResponseError as e:
print(f"[notion] Push failed: {e}")
return False
def sync(db_id: str, items: list[dict]) -> tuple[int, int]:
existing = get_existing_urls(db_id)
added = skipped = 0
for item in items:
if item.get("url") in existing:
skipped += 1; continue
if push_item(db_id, item):
added += 1; existing.add(item["url"])
else:
skipped += 1
return added, skipped
```
***
### 步骤 8: 在 main.py 中编排
```python
# scraper/main.py
import os, sys, yaml
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
from scraper.sources import my_source # add your sources
# NOTE: This example uses Notion. If storage.provider is "sheets" or "supabase",
# replace this import with storage.sheets_sync or storage.supabase_sync and update
# the env var and sync() call accordingly.
from storage.notion_sync import sync
SOURCES = [
("My Source", my_source.fetch),
]
def ai_enabled():
return bool(os.environ.get("GEMINI_API_KEY"))
def main():
config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
provider = config.get("storage", {}).get("provider", "notion")
# Resolve the storage target identifier from env based on provider
if provider == "notion":
db_id = os.environ.get("NOTION_DATABASE_ID")
if not db_id:
print("ERROR: NOTION_DATABASE_ID not set"); sys.exit(1)
else:
# Extend here for sheets (SHEET_ID) or supabase (SUPABASE_TABLE) etc.
print(f"ERROR: provider '{provider}' not yet wired in main.py"); sys.exit(1)
config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
all_items = []
for name, fetch_fn in SOURCES:
try:
items = fetch_fn()
print(f"[{name}] {len(items)} items")
all_items.extend(items)
except Exception as e:
print(f"[{name}] FAILED: {e}")
# Deduplicate by URL
seen, deduped = set(), []
for item in all_items:
if (url := item.get("url", "")) and url not in seen:
seen.add(url); deduped.append(item)
print(f"Unique items: {len(deduped)}")
if ai_enabled() and deduped:
from ai.memory import load_feedback, build_preference_prompt
from ai.pipeline import analyse_batch
# load_feedback() reads data/feedback.json written by your feedback sync script.
# To keep it current, implement a separate feedback_sync.py that queries your
# storage provider for items with positive/negative statuses and calls save_feedback().
feedback = load_feedback()
preference = build_preference_prompt(feedback)
context_path = Path(__file__).parent.parent / "profile" / "context.md"
context = context_path.read_text() if context_path.exists() else ""
deduped = analyse_batch(deduped, context=context, preference_prompt=preference)
else:
print("[AI] Skipped — GEMINI_API_KEY not set")
added, skipped = sync(db_id, deduped)
print(f"Done — {added} new, {skipped} existing")
if __name__ == "__main__":
main()
```
***
### 步骤 9: GitHub Actions工作流
```yaml
# .github/workflows/scraper.yml
name: Data Scraper Agent
on:
schedule:
- cron: "0 */3 * * *" # every 3 hours — adjust to your needs
workflow_dispatch: # allow manual trigger
permissions:
contents: write # required for the feedback-history commit step
jobs:
scrape:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- run: pip install -r requirements.txt
# Uncomment if Playwright is enabled in requirements.txt
# - name: Install Playwright browsers
# run: python -m playwright install chromium --with-deps
- name: Run agent
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: python -m scraper.main
- name: Commit feedback history
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/feedback.json || true
git diff --cached --quiet || git commit -m "chore: update feedback history"
git push
```
***
### 步骤 10: config.yaml 模板
```yaml
# Customise this file — no code changes needed
# What to collect (pre-filter before AI)
filters:
required_keywords: [] # item must contain at least one
blocked_keywords: [] # item must not contain any
# Your priorities — AI uses these for scoring
priorities:
- "example priority 1"
- "example priority 2"
# Storage
storage:
provider: "notion" # notion | sheets | supabase | sqlite
# Feedback learning
feedback:
positive_statuses: ["Saved", "Applied", "Interested"]
negative_statuses: ["Skip", "Rejected", "Not relevant"]
# AI settings
ai:
enabled: true
model: "gemini-2.5-flash"
min_score: 0 # filter out items below this score
rate_limit_seconds: 7 # seconds between API calls
batch_size: 5 # items per API call
```
***
## 常见抓取模式
### 模式 1: REST API最简单
```python
resp = requests.get(url, params={"q": query}, headers=HEADERS, timeout=15)
items = resp.json().get("results", [])
```
### 模式 2: HTML抓取
```python
soup = BeautifulSoup(resp.text, "lxml")
for card in soup.select(".listing-card"):
title = card.select_one("h2").get_text(strip=True)
href = card.select_one("a")["href"]
```
### 模式 3: RSS源
```python
import xml.etree.ElementTree as ET
root = ET.fromstring(resp.text)
for item in root.findall(".//item"):
title = item.findtext("title", "")
link = item.findtext("link", "")
pub_date = item.findtext("pubDate", "")
```
### 模式 4: 分页API
```python
page = 1
while True:
resp = requests.get(url, params={"page": page, "limit": 50}, timeout=15)
data = resp.json()
items = data.get("results", [])
if not items:
break
for item in items:
results.append(_normalise(item))
if not data.get("has_more"):
break
page += 1
```
### 模式 5: JS渲染页面Playwright
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url)
page.wait_for_selector(".listing")
html = page.content()
browser.close()
soup = BeautifulSoup(html, "lxml")
```
***
## 需要避免的反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
| 每个项目调用一次LLM | 立即达到速率限制 | 每次调用批量处理5个项目 |
| 代码中硬编码关键字 | 不可重用 | 将所有配置移动到 `config.yaml` |
| 没有速率限制的抓取 | IP被禁止 | 在请求之间添加 `time.sleep(1)` |
| 在代码中存储密钥 | 安全风险 | 始终使用 `.env` + GitHub Secrets |
| 没有去重 | 重复行堆积 | 在推送前始终检查URL |
| 忽略 `robots.txt` | 法律/道德风险 | 遵守爬虫规则尽可能使用公共API |
| 使用 `requests` 处理JS渲染的网站 | 空响应 | 使用Playwright或查找底层API |
| `maxOutputTokens` 太低 | JSON截断解析错误 | 对批量响应使用2048+ |
***
## 免费层级限制参考
| 服务 | 免费限制 | 典型用法 |
|---|---|---|
| Gemini Flash Lite | 30 RPM, 1500 RPD | 以3小时间隔约56次请求/天 |
| Gemini 2.0 Flash | 15 RPM, 1500 RPD | 良好的后备选项 |
| Gemini 2.5 Flash | 10 RPM, 500 RPD | 谨慎使用 |
| GitHub Actions | 无限(公共仓库) | 约20分钟/天 |
| Notion API | 无限 | 约200次写入/天 |
| Supabase | 500MB DB, 2GB传输 | 适用于大多数代理 |
| Google Sheets API | 300次请求/分钟 | 适用于小型代理 |
***
## 需求模板
```
requests==2.31.0
beautifulsoup4==4.12.3
lxml==5.1.0
python-dotenv==1.0.1
pyyaml==6.0.2
notion-client==2.2.1 # if using Notion
# playwright==1.40.0 # uncomment for JS-rendered sites
```
***
## 质量检查清单
在将代理标记为完成之前:
* \[ ] `config.yaml` 控制所有面向用户的设置 — 没有硬编码的值
* \[ ] `profile/context.md` 保存用于AI匹配的用户特定上下文
* \[ ] 在每次存储推送前通过URL进行去重
* \[ ] Gemini客户端具有模型后备链4个模型
* \[ ] 批量大小 ≤ 每个API调用5个项目
* \[ ] `maxOutputTokens` ≥ 2048
* \[ ] `.env` 在 `.gitignore` 中
* \[ ] 提供了用于入门的 `.env.example`
* \[ ] `setup.py` 在首次运行时创建数据库模式
* \[ ] `enrich_existing.py` 回填旧行的AI分数
* \[ ] GitHub Actions工作流在每次运行后提交 `feedback.json`
* \[ ] README涵盖在<5分钟内设置所需的密钥自定义
***
## 真实世界示例
```
"Build me an agent that monitors Hacker News for AI startup funding news"
"Scrape product prices from 3 e-commerce sites and alert when they drop"
"Track new GitHub repos tagged with 'llm' or 'agents' — summarise each one"
"Collect Chief of Staff job listings from LinkedIn and Cutshort into Notion"
"Monitor a subreddit for posts mentioning my company — classify sentiment"
"Scrape new academic papers from arXiv on a topic I care about daily"
"Track sports fixture results and keep a running table in Google Sheets"
"Build a real estate listing watcher — alert on new properties under ₹1 Cr"
```
***
## 参考实现
一个使用此确切架构构建的完整工作代理将抓取4+个数据源,
批量处理Gemini调用从存储在Notion中的"已应用"/"已拒绝"决策中学习,并且
在GitHub Actions上100%免费运行。按照上述步骤1-9构建您自己的代理。

View File

@@ -151,7 +151,7 @@ node scripts/orchestrate-worktrees.js plan.json --execute
{
"sessionName": "skill-audit",
"baseRef": "HEAD",
"launcherCommand": "codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}",
"launcherCommand": "codex exec --cwd {worktree_path} --task-file {task_file}",
"workers": [
{ "name": "docs-a", "task": "Fix skills 1-4 and write handoff notes." },
{ "name": "docs-b", "task": "Fix skills 5-8 and write handoff notes." }
@@ -178,7 +178,7 @@ node scripts/orchestrate-worktrees.js plan.json --execute
"scripts/lib/tmux-worktree-orchestrator.js",
".claude/plan/workflow-e2e-test.json"
],
"launcherCommand": "bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}",
"launcherCommand": "bash {repo_root}/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}",
"workers": [
{ "name": "seed-check", "task": "Verify seeded files are present before starting work." }
]

View File

@@ -0,0 +1,90 @@
---
name: documentation-lookup
description: 通过 Context7 MCP 使用最新的库和框架文档而非训练数据。当用户提出设置问题、API参考、代码示例或命名框架例如 React、Next.js、Prisma时激活。
origin: ECC
---
# 文档查询 (Context7)
当用户询问库、框架或 API 时,通过 Context7 MCP工具 `resolve-library-id``query-docs`)获取最新文档,而非依赖训练数据。
## 核心概念
* **Context7**: 提供实时文档的 MCP 服务器;用于库和 API 的查询,替代训练数据。
* **resolve-library-id**: 根据库名和查询返回 Context7 兼容的库 ID例如 `/vercel/next.js`)。
* **query-docs**: 根据给定的库 ID 和问题获取文档和代码片段。务必先调用 resolve-library-id 以获取有效的库 ID。
## 使用时机
当用户出现以下情况时激活:
* 询问设置或配置问题(例如“如何配置 Next.js 中间件?”)
* 请求依赖于某个库的代码(“编写一个 Prisma 查询用于...”)
* 需要 API 或参考信息“Supabase 的认证方法有哪些?”)
* 提及特定的框架或库React、Vue、Svelte、Express、Tailwind、Prisma、Supabase 等)
当请求依赖于库、框架或 API 的准确、最新行为时,请使用此技能。适用于配置了 Context7 MCP 的所有环境(例如 Claude Code、Cursor、Codex
## 工作原理
### 步骤 1解析库 ID
调用 **resolve-library-id** MCP 工具,参数包括:
* **libraryName**: 从用户问题中提取的库或产品名称(例如 `Next.js``Prisma``Supabase`)。
* **query**: 用户的完整问题。这有助于提高结果的相关性排名。
在查询文档之前,必须获取 Context7 兼容的库 ID格式为 `/org/project``/org/project/version`)。如果没有从此步骤获得有效的库 ID请勿调用 query-docs。
### 步骤 2选择最佳匹配
从解析结果中,根据以下原则选择一个结果:
* **名称匹配**: 优先选择与用户询问内容完全匹配或最接近的。
* **基准分数**: 分数越高表示文档质量越好(最高为 100
* **来源信誉**: 如果可用,优先选择信誉度为 High 或 Medium 的。
* **版本**: 如果用户指定了版本例如“React 19”、“Next.js 15”优先选择列出的特定版本库 ID例如 `/org/project/v1.2.0`)。
### 步骤 3获取文档
调用 **query-docs** MCP 工具,参数包括:
* **libraryId**: 从步骤 2 中选择的 Context7 库 ID例如 `/vercel/next.js`)。
* **query**: 用户的具体问题或任务。为获得相关片段,请具体描述。
限制:每个问题调用 query-docs或 resolve-library-id的次数不要超过 3 次。如果 3 次调用后答案仍不明确,请说明不确定性并使用您掌握的最佳信息,而不是猜测。
### 步骤 4使用文档
* 使用获取的、最新的信息回答用户的问题。
* 在有用时包含文档中的相关代码示例。
* 在重要时引用库或版本(例如“在 Next.js 15 中...”)。
## 示例
### 示例Next.js 中间件
1. 使用 `libraryName: "Next.js"``query: "How do I set up Next.js middleware?"` 调用 **resolve-library-id**
2. 从结果中,根据名称和基准分数选择最佳匹配(例如 `/vercel/next.js`)。
3. 使用 `libraryId: "/vercel/next.js"``query: "How do I set up Next.js middleware?"` 调用 **query-docs**
4. 使用返回的片段和文本来回答;如果相关,包含文档中的一个最小 `middleware.ts` 示例。
### 示例Prisma 查询
1. 使用 `libraryName: "Prisma"``query: "How do I query with relations?"` 调用 **resolve-library-id**
2. 选择官方的 Prisma 库 ID例如 `/prisma/prisma`)。
3. 使用该 `libraryId` 和查询调用 **query-docs**
4. 返回 Prisma Client 模式(例如 `include``select`)并附上文档中的简短代码片段。
### 示例Supabase 认证方法
1. 使用 `libraryName: "Supabase"``query: "What are the auth methods?"` 调用 **resolve-library-id**
2. 选择 Supabase 文档库 ID。
3. 调用 **query-docs**;总结认证方法并展示从获取的文档中得到的最小示例。
## 最佳实践
* **具体化**: 尽可能使用用户的完整问题作为查询,以获得更好的相关性。
* **版本意识**: 当用户提及版本时,如果可用,在解析步骤中使用特定版本的库 ID。
* **优先官方来源**: 当存在多个匹配项时,优先选择官方或主要包,而非社区分支。
* **无敏感数据**: 从发送到 Context7 的任何查询中,删除 API 密钥、密码、令牌和其他机密信息。在将用户问题传递给 resolve-library-id 或 query-docs 之前,将其视为可能包含机密信息。

View File

@@ -24,17 +24,14 @@ origin: ECC
```json
"exa-web-search": {
"command": "npx",
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"args": ["-y", "exa-mcp-server"],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
```
在 [exa.ai](https://exa.ai) 获取 API 密钥。
如果省略 `tools=...` 参数,可能只会启用较小的默认工具集
此仓库当前的 Exa 设置记录了此处公开的工具接口:`web_search_exa``get_code_context_exa`
如果你的 Exa 服务器公开了其他工具,请在文档或提示中依赖它们之前,先核实其确切名称。
## 核心工具
@@ -50,32 +47,11 @@ web_search_exa(query: "latest AI developments 2026", numResults: 5)
| 参数 | 类型 | 默认值 | 说明 |
|-------|------|---------|-------|
| `query` | string | 必 | 搜索查询 |
| `numResults` | number | 8 | 结果数量 |
### web\_search\_advanced\_exa
具有域名和日期约束的过滤搜索。
```
web_search_advanced_exa(
query: "React Server Components best practices",
numResults: 5,
includeDomains: ["github.com", "react.dev"],
startPublishedDate: "2025-01-01"
)
```
**参数:**
| 参数 | 类型 | 默认值 | 说明 |
|-------|------|---------|-------|
| `query` | string | 必需 | 搜索查询 |
| `numResults` | number | 8 | 结果数量 |
| `includeDomains` | string\[] | 无 | 限制在特定域名 |
| `excludeDomains` | string\[] | 无 | 排除特定域名 |
| `startPublishedDate` | string | 无 | ISO 日期过滤器(开始) |
| `endPublishedDate` | string | 无 | ISO 日期过滤器(结束) |
| `query` | 字符串 | 必 | 搜索查询 |
| `numResults` | 数字 | 8 | 结果数量 |
| `type` | 字符串 | `auto` | 搜索模式 |
| `livecrawl` | 字符串 | `fallback` | 需要时优先使用实时爬取 |
| `category` | 字符串 | 无 | 可选焦点,例如 `company``research paper` |
### get\_code\_context\_exa
@@ -92,56 +68,6 @@ get_code_context_exa(query: "Python asyncio patterns", tokensNum: 3000)
| `query` | string | 必需 | 代码或 API 搜索查询 |
| `tokensNum` | number | 5000 | 内容令牌数1000-50000 |
### company\_research\_exa
用于商业情报和新闻的公司研究。
```
company_research_exa(companyName: "Anthropic", numResults: 5)
```
**参数:**
| 参数 | 类型 | 默认值 | 说明 |
|-------|------|---------|-------|
| `companyName` | string | 必需 | 公司名称 |
| `numResults` | number | 5 | 结果数量 |
### people\_search\_exa
查找专业资料和个人简介。
```
people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
```
### crawling\_exa
从 URL 提取完整页面内容。
```
crawling_exa(url: "https://example.com/article", tokensNum: 5000)
```
**参数:**
| 参数 | 类型 | 默认值 | 说明 |
|-------|------|---------|-------|
| `url` | string | 必需 | 要提取的 URL |
| `tokensNum` | number | 5000 | 内容令牌数 |
### deep\_researcher\_start / deep\_researcher\_check
启动一个异步运行的 AI 研究代理。
```
# Start research
deep_researcher_start(query: "comprehensive analysis of AI code editors in 2026")
# Check status (returns results when complete)
deep_researcher_check(researchId: "<id from start>")
```
## 使用模式
### 快速查找
@@ -156,29 +82,26 @@ web_search_exa(query: "Node.js 22 new features", numResults: 3)
get_code_context_exa(query: "Rust error handling patterns Result type", tokensNum: 3000)
```
### 公司尽职调查
### 公司或人物研究
```
company_research_exa(companyName: "Vercel", numResults: 5)
web_search_advanced_exa(query: "Vercel funding valuation 2026", numResults: 3)
web_search_exa(query: "Vercel funding valuation 2026", numResults: 3, category: "company")
web_search_exa(query: "site:linkedin.com/in AI safety researchers Anthropic", numResults: 5)
```
### 技术深度研究
```
# Start async research
deep_researcher_start(query: "WebAssembly component model status and adoption")
# ... do other work ...
deep_researcher_check(researchId: "<id>")
web_search_exa(query: "WebAssembly component model status and adoption", numResults: 5)
get_code_context_exa(query: "WebAssembly component model examples", tokensNum: 4000)
```
## 提示
* 使用 `web_search_exa` 进行广泛查询,使用 `web_search_advanced_exa` 获取过滤结果
* 较低的 `tokensNum`1000-2000用于聚焦的代码片段较高的5000+)用于全面的上下文
* 结合 `company_research_exa``web_search_advanced_exa` 进行彻底的公司分析
* 使用 `crawling_exa` 从搜索结果中的特定 URL 获取完整内容
* `deep_researcher_start` 最适合受益于 AI 综合的全面主题
* 使用 `web_search_exa` 获取最新信息、公司查询和广泛发现
* 使用 `site:`、引号内的短语和 `intitle:` 等搜索运算符来缩小结果范围
* 对于聚焦的代码片段,使用较低的 `tokensNum` (1000-2000);对于全面的上下文,使用较高的值 (5000+)
* 当你需要 API 用法或代码示例而非通用网页时,使用 `get_code_context_exa`
## 相关技能

View File

@@ -0,0 +1,480 @@
---
name: flutter-dart-code-review
description: 库无关的Flutter/Dart代码审查清单涵盖Widget最佳实践、状态管理模式BLoC、Riverpod、Provider、GetX、MobX、Signals、Dart惯用法、性能、可访问性、安全性和整洁架构。
origin: ECC
---
# Flutter/Dart 代码审查最佳实践
适用于审查 Flutter/Dart 应用程序的全面、与库无关的清单。无论使用哪种状态管理方案、路由库或依赖注入框架,这些原则都适用。
***
## 1. 通用项目健康度
* \[ ] 项目遵循一致的文件夹结构(功能优先或分层优先)
* \[ ] 关注点分离得当UI、业务逻辑、数据层
* \[ ] 部件中无业务逻辑;部件纯粹是展示性的
* \[ ] `pubspec.yaml` 是干净的 —— 没有未使用的依赖项,版本已适当固定
* \[ ] `analysis_options.yaml` 包含严格的 lint 规则集,并启用了严格的分析器设置
* \[ ] 生产代码中没有 `print()` 语句 —— 使用 `dart:developer` `log()` 或日志包
* \[ ] 生成的文件 (`.g.dart`, `.freezed.dart`, `.gr.dart`) 是最新的或在 `.gitignore`
* \[ ] 平台特定代码通过抽象进行隔离
***
## 2. Dart 语言陷阱
* \[ ] **隐式动态类型**:缺少类型注解导致 `dynamic` —— 启用 `strict-casts`, `strict-inference`, `strict-raw-types`
* \[ ] **空安全误用**:过度使用 `!`(感叹号操作符)而不是适当的空检查或 Dart 3 模式匹配 (`if (value case var v?)`)
* \[ ] **类型提升失败**:在可以使用局部变量类型提升的地方使用了 `this.field`
* \[ ] **捕获范围过宽**`catch (e)` 没有 `on` 子句;应始终指定异常类型
* \[ ] **捕获 `Error`**`Error` 子类型表示错误,不应被捕获
* \[ ] **未使用的 `async`**:标记为 `async` 但从未 `await` 的函数 —— 不必要的开销
* \[ ] **`late` 过度使用**:在可使用可空类型或构造函数初始化更安全的地方使用了 `late`;将错误推迟到运行时
* \[ ] **循环中的字符串拼接**:使用 `StringBuffer` 而不是 `+` 进行迭代式字符串构建
* \[ ] **`const` 上下文中的可变状态**`const` 构造器类中的字段不应是可变的
* \[ ] **忽略 `Future` 返回值**:使用 `await` 或显式调用 `unawaited()` 来表明意图
* \[ ] **在 `final` 可用时使用 `var`**:局部变量首选 `final`,编译时常量首选 `const`
* \[ ] **相对导入**:为保持一致性,使用 `package:` 导入
* \[ ] **暴露可变集合**:公共 API 应返回不可修改的视图,而不是原始的 `List`/`Map`
* \[ ] **缺少 Dart 3 模式匹配**:优先使用 switch 表达式和 `if-case`,而不是冗长的 `is` 检查和手动类型转换
* \[ ] **为多重返回值使用一次性类**:使用 Dart 3 记录 `(String, int)` 代替一次性 DTO
* \[ ] **生产代码中的 `print()`**:使用 `dart:developer` `log()` 或项目的日志包;`print()` 没有日志级别且无法过滤
***
## 3. 部件最佳实践
### 部件分解:
* \[ ] 没有单个部件的 `build()` 方法超过约 80-100 行
* \[ ] 部件按封装方式以及按变化方式(重建边界)进行拆分
* \[ ] 返回部件的私有 `_build*()` 辅助方法被提取到单独的部件类中(支持元素重用、常量传播和框架优化)
* \[ ] 在不需要可变局部状态的地方,优先使用无状态部件而非有状态部件
* \[ ] 提取的部件在可复用时放在单独的文件中
### Const 使用:
* \[ ] 尽可能使用 `const` 构造器 —— 防止不必要的重建
* \[ ] 对不变化的集合使用 `const` 字面量 (`const []`, `const {}`)
* \[ ] 当所有字段都是 final 时,构造函数声明为 `const`
### Key 使用:
* \[ ] 在列表/网格中使用 `ValueKey` 以在重新排序时保持状态
* \[ ] 谨慎使用 `GlobalKey` —— 仅在确实需要跨树访问状态时使用
* \[ ] 避免在 `build()` 中使用 `UniqueKey` —— 它会强制每帧都重建
* \[ ] 当身份基于数据对象而非单个值时,使用 `ObjectKey`
### 主题与设计系统:
* \[ ] 颜色来自 `Theme.of(context).colorScheme` —— 没有硬编码的 `Colors.red` 或十六进制值
* \[ ] 文本样式来自 `Theme.of(context).textTheme` —— 没有内联的 `TextStyle` 和原始字体大小
* \[ ] 已验证深色模式兼容性 —— 不假设浅色背景
* \[ ] 间距和尺寸使用一致的设计令牌或常量,而不是魔法数字
### Build 方法复杂度:
* \[ ] `build()` 中没有网络调用、文件 I/O 或繁重计算
* \[ ] `build()` 中没有 `Future.then()``async` 工作
* \[ ] `build()` 中没有创建订阅 (`.listen()`)
* \[ ] `setState()` 局部化到尽可能小的子树
***
## 4. 状态管理(与库无关)
这些原则适用于所有 Flutter 状态管理方案BLoC、Riverpod、Provider、GetX、MobX、Signals、ValueNotifier 等)。
### 架构:
* \[ ] 业务逻辑位于部件层之外 —— 在状态管理组件中BLoC、Notifier、Controller、Store、ViewModel 等)
* \[ ] 状态管理器通过依赖注入接收依赖,而不是内部构造它们
* \[ ] 服务或仓库层抽象数据源 —— 部件和状态管理器不应直接调用 API 或数据库
* \[ ] 状态管理器职责单一 —— 没有处理不相关职责的“上帝”管理器
* \[ ] 跨组件依赖遵循解决方案的约定:
***Riverpod** 中:提供者通过 `ref.watch` 依赖其他提供者是预期的 —— 仅标记循环或过度复杂的链
***BLoC**bloc 不应直接依赖其他 bloc —— 优先使用共享仓库或表示层协调
* 在其他解决方案中:遵循文档中关于组件间通信的约定
### 不可变性与值相等性适用于不可变状态解决方案BLoC、Riverpod、Redux
* \[ ] 状态对象是不可变的 —— 通过 `copyWith()` 或构造函数创建新实例,绝不就地修改
* \[ ] 状态类正确实现 `==``hashCode`(比较中包含所有字段)
* \[ ] 机制在整个项目中保持一致 —— 手动覆盖、`Equatable``freezed`、Dart 记录或其他方式
* \[ ] 状态对象内部的集合不作为原始可变的 `List`/`Map` 暴露
### 响应式纪律适用于响应式突变解决方案MobX、GetX、Signals
* \[ ] 状态仅通过解决方案的响应式 API 进行修改MobX 中的 `@action`Signals 上的 `.value`GetX 中的 `.obs`)—— 直接字段修改会绕过变更跟踪
* \[ ] 派生值使用解决方案的计算机制,而不是冗余存储
* \[ ] 反应和清理器被正确清理MobX 中的 `ReactionDisposer`Signals 中的 effect 清理)
### 状态形状设计:
* \[ ] 互斥状态使用密封类型、联合变体或解决方案内置的异步状态类型(例如 Riverpod 的 `AsyncValue`)—— 而不是布尔标志 (`isLoading`, `isError`, `hasData`)
* \[ ] 每个异步操作都将加载、成功和错误建模为不同的状态
* \[ ] UI 中详尽处理所有状态变体 —— 没有静默忽略的情况
* \[ ] 错误状态携带用于显示的错误信息;加载状态不携带陈旧数据
* \[ ] 可空数据不用于作为加载指示器 —— 状态是明确的
```dart
// BAD — boolean flag soup allows impossible states
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError is representable!
User? user;
}
// GOOD (immutable approach) — sealed types make impossible states unrepresentable
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// GOOD (reactive approach) — observable enum + data, mutations via reactivity API
// enum UserStatus { initial, loading, loaded, error }
// Use your solution's observable/signal to wrap status and data separately
```
### 重建优化:
* \[ ] 状态消费者部件Builder、Consumer、Observer、Obx、Watch 等)的范围尽可能窄
* \[ ] 使用选择器仅在特定字段变化时重建 —— 而不是每次状态发射时
* \[ ] 使用 `const` 部件来阻止重建在树中传播
* \[ ] 计算/派生状态是响应式计算的,而不是冗余存储的
### 订阅与清理:
* \[ ] 所有手动订阅 (`.listen()`) 在 `dispose()` / `close()` 中被取消
* \[ ] 流控制器在不再需要时关闭
* \[ ] 定时器在清理生命周期中被取消
* \[ ] 优先使用框架管理的生命周期,而不是手动订阅(声明式构建器优于 `.listen()`
* \[ ] 异步回调中在 `setState` 之前检查 `mounted`
* \[ ] 在 `await` 之后使用 `BuildContext` 而不检查 `context.mounted`Flutter 3.7+)—— 过时的上下文会导致崩溃
* \[ ] 在异步间隙后,没有在验证部件仍然挂载的情况下进行导航、显示对话框或脚手架消息
* \[ ] `BuildContext` 绝不存储在单例、状态管理器或静态字段中
### 本地状态与全局状态:
* \[ ] 临时 UI 状态(复选框、滑块、动画)使用本地状态 (`setState`, `ValueNotifier`)
* \[ ] 共享状态仅提升到所需的高度 —— 不过度全局化
* \[ ] 功能作用域的状态在功能不再活跃时被正确清理
***
## 5. 性能
### 不必要的重建:
* \[ ] 不在根部件级别调用 `setState()` —— 将状态变化局部化
* \[ ] 使用 `const` 部件来阻止重建传播
* \[ ] 在独立重绘的复杂子树周围使用 `RepaintBoundary`
* \[ ] 使用 `AnimatedBuilder` 的 child 参数处理独立于动画的子树
### build() 中的昂贵操作:
* \[ ] 不在 `build()` 中对大型集合进行排序、过滤或映射 —— 在状态管理层计算
* \[ ] 不在 `build()` 中编译正则表达式
* \[ ] `MediaQuery.of(context)` 的使用是具体的(例如,`MediaQuery.sizeOf(context)`
### 图像优化:
* \[ ] 网络图像使用缓存(适用于项目的任何缓存解决方案)
* \[ ] 为目标设备使用适当的图像分辨率(不为缩略图加载 4K 图像)
* \[ ] 使用带有 `cacheWidth`/`cacheHeight``Image.asset` 以按显示尺寸解码
* \[ ] 为网络图像提供占位符和错误部件
### 懒加载:
* \[ ] 对于大型或动态列表,使用 `ListView.builder` / `GridView.builder` 代替 `ListView(children: [...])`(对于小型、静态列表,具体构造器是可以的)
* \[ ] 为大型数据集实现分页
* \[ ] 在 Web 构建中对重量级库使用延迟加载 (`deferred as`)
### 其他:
* \[ ] 在动画中避免使用 `Opacity` 部件 —— 使用 `AnimatedOpacity``FadeTransition`
* \[ ] 在动画中避免裁剪 —— 预裁剪图像
* \[ ] 不在部件上重写 `operator ==` —— 使用 `const` 构造器代替
* \[ ] 固有尺寸部件 (`IntrinsicHeight`, `IntrinsicWidth`) 谨慎使用(额外的布局传递)
***
## 6. 测试
### 测试类型与期望:
* \[ ] **单元测试**:覆盖所有业务逻辑(状态管理器、仓库、工具函数)
* \[ ] **部件测试**:覆盖单个部件的行为、交互和视觉输出
* \[ ] **集成测试**:端到端覆盖关键用户流程
* \[ ] **Golden 测试**:对设计关键的 UI 组件进行像素级精确比较
### 覆盖率目标:
* \[ ] 业务逻辑的目标行覆盖率达到 80% 以上
* \[ ] 所有状态转换都有对应的测试(加载 → 成功,加载 → 错误,重试等)
* \[ ] 测试边缘情况:空状态、错误状态、加载状态、边界值
### 测试隔离:
* \[ ] 外部依赖API 客户端、数据库、服务)已被模拟或伪造
* \[ ] 每个测试文件仅测试一个类/单元
* \[ ] 测试验证行为,而非实现细节
* \[ ] 存根仅定义每个测试所需的行为(最小化存根)
* \[ ] 测试用例之间没有共享的可变状态
### 小部件测试质量:
* \[ ] `pumpWidget``pump` 被正确用于异步操作
* \[ ] `find.byType``find.text``find.byKey` 使用得当
* \[ ] 没有依赖于时序的不可靠测试——使用 `pumpAndSettle` 或显式的 `pump(Duration)`
* \[ ] 测试在 CI 中运行,失败会阻止合并
***
## 7. 无障碍功能
### 语义化小部件:
* \[ ] 使用 `Semantics` 小部件在自动标签不足时提供屏幕阅读器标签
* \[ ] 使用 `ExcludeSemantics` 处理纯装饰性元素
* \[ ] 使用 `MergeSemantics` 将相关小部件组合成单个可访问元素
* \[ ] 图像设置了 `semanticLabel` 属性
### 屏幕阅读器支持:
* \[ ] 所有交互元素均可聚焦并具有有意义的描述
* \[ ] 焦点顺序符合逻辑(遵循视觉阅读顺序)
### 视觉无障碍:
* \[ ] 文本与背景的对比度 >= 4.5:1
* \[ ] 可点击目标至少为 48x48 像素
* \[ ] 颜色不是状态的唯一指示器(同时使用图标/文本)
* \[ ] 文本随系统字体大小设置缩放
### 交互无障碍:
* \[ ] 没有无操作的 `onPressed` 回调——每个按钮都有作用或处于禁用状态
* \[ ] 错误字段建议更正
* \[ ] 用户输入数据时,上下文不会意外改变
***
## 8. 平台特定考量
### iOS/Android 差异:
* \[ ] 在适当的地方使用平台自适应小部件
* \[ ] 返回导航处理正确Android 返回按钮iOS 滑动返回)
* \[ ] 通过 `SafeArea` 小部件处理状态栏和安全区域
* \[ ] 平台特定权限在 `AndroidManifest.xml``Info.plist` 中声明
### 响应式设计:
* \[ ] 使用 `LayoutBuilder``MediaQuery` 实现响应式布局
* \[ ] 断点定义一致(手机、平板、桌面)
* \[ ] 文本在小屏幕上不会溢出——使用 `Flexible``Expanded``FittedBox`
* \[ ] 测试了横屏方向或明确锁定
* \[ ] Web 特定:支持鼠标/键盘交互,存在悬停状态
***
## 9. 安全性
### 安全存储:
* \[ ] 敏感数据令牌、凭证使用平台安全存储存储iOS 上的 KeychainAndroid 上的 EncryptedSharedPreferences
* \[ ] 从不以明文存储机密信息
* \[ ] 对于敏感操作考虑使用生物识别认证门控
### API 密钥处理:
* \[ ] API 密钥 NOT 硬编码在 Dart 源代码中——使用 `--dart-define``.env` 文件从 VCS 中排除,或使用编译时配置
* \[ ] 机密信息未提交到 git——检查 `.gitignore`
* \[ ] 对真正的秘密密钥使用后端代理(客户端不应持有服务器机密)
### 输入验证:
* \[ ] 所有用户输入在发送到 API 前都经过验证
* \[ ] 表单验证使用适当的验证模式
* \[ ] 没有原始 SQL 或用户输入的字符串插值
* \[ ] 深度链接 URL 在导航前经过验证和清理
### 网络安全:
* \[ ] 所有 API 调用强制使用 HTTPS
* \[ ] 对于高安全性应用考虑证书锁定
* \[ ] 认证令牌正确刷新和过期
* \[ ] 没有记录或打印敏感数据
***
## 10. 包/依赖项审查
### 评估 pub.dev 包:
* \[ ] 检查 **pub 分数**(目标 130+/160
* \[ ] 检查 **点赞数**和**流行度**作为社区信号
* \[ ] 验证发布者在 pub.dev 上**已验证**
* \[ ] 检查最后发布日期——过时的包(>1 年)有风险
* \[ ] 审查维护者的未解决问题和响应时间
* \[ ] 检查许可证与项目的兼容性
* \[ ] 验证平台支持是否覆盖您的目标
### 版本约束:
* \[ ] 对依赖项使用插入符语法(`^1.2.3`)——允许兼容性更新
* \[ ] 仅在绝对必要时固定确切版本
* \[ ] 定期运行 `flutter pub outdated` 以跟踪过时的依赖项
* \[ ] 生产 `pubspec.yaml` 中没有依赖项覆盖——仅用于带有注释/问题链接的临时修复
* \[ ] 最小化传递依赖项数量——每个依赖项都是一个攻击面
### 单仓库特定melos/workspace
* \[ ] 内部包仅从公共 API 导入——没有 `package:other/src/internal.dart`(破坏 Dart 包封装)
* \[ ] 内部包依赖项使用工作区解析,而不是硬编码的 `path: ../../` 相对字符串
* \[ ] 所有子包共享或继承根 `analysis_options.yaml`
***
## 11. 导航和路由
### 通用原则(适用于任何路由解决方案):
* \[ ] 一致使用一种路由方法——不混合命令式 `Navigator.push` 和声明式路由器
* \[ ] 路由参数是类型化的——没有 `Map<String, dynamic>``Object?` 转换
* \[ ] 路由路径定义为常量、枚举或生成——没有散布在代码中的魔法字符串
* \[ ] 认证守卫/重定向集中化——不在各个屏幕中重复
* \[ ] 为 Android 和 iOS 配置深度链接
* \[ ] 深度链接 URL 在导航前经过验证和清理
* \[ ] 导航状态是可测试的——可以在测试中验证路由更改
* \[ ] 在所有平台上返回行为正确
***
## 12. 错误处理
### 框架错误处理:
* \[ ] 重写 `FlutterError.onError` 以捕获框架错误(构建、布局、绘制)
* \[ ] 设置 `PlatformDispatcher.instance.onError` 处理 Flutter 未捕获的异步错误
* \[ ] 为发布模式自定义 `ErrorWidget.builder`(用户友好而非红屏)
* \[ ] 在 `runApp` 周围使用全局错误捕获包装器(例如 `runZonedGuarded`Sentry/Crashlytics 包装器)
### 错误报告:
* \[ ] 集成了错误报告服务Firebase Crashlytics、Sentry 或等效服务)
* \[ ] 报告非致命错误并附上堆栈跟踪
* \[ ] 状态管理错误观察器连接到错误报告例如BlocObserver、ProviderObserver 或适用于您解决方案的等效项)
* \[ ] 为调试目的,将用户可识别信息(用户 ID附加到错误报告
### 优雅降级:
* \[ ] API 错误导致用户友好的错误 UI而非崩溃
* \[ ] 针对瞬时网络故障的重试机制
* \[ ] 优雅处理离线状态
* \[ ] 状态管理中的错误状态携带用于显示的错误信息
* \[ ] 原始异常(网络、解析)在到达 UI 之前被映射为用户友好的本地化消息——从不向用户显示原始异常字符串
***
## 13. 国际化l10n
### 设置:
* \[ ] 配置了本地化解决方案Flutter 内置的 ARB/l10n、easy\_localization 或等效方案)
* \[ ] 在应用配置中声明了支持的语言环境
### 内容:
* \[ ] 所有用户可见字符串都使用本地化系统——小部件中没有硬编码字符串
* \[ ] 模板文件包含翻译人员的描述/上下文
* \[ ] 使用 ICU 消息语法处理复数、性别、选择
* \[ ] 使用类型定义占位符
* \[ ] 跨语言环境没有缺失的键
### 代码审查:
* \[ ] 在整个项目中一致使用本地化访问器
* \[ ] 日期、时间、数字和货币格式化具有语言环境感知能力
* \[ ] 如果目标语言是阿拉伯语、希伯来语等则支持文本方向性RTL
* \[ ] 本地化文本没有字符串拼接——使用参数化消息
***
## 14. 依赖注入
### 原则(适用于任何 DI 方法):
* \[ ] 类在层边界上依赖于抽象(接口),而不是具体实现
* \[ ] 依赖项通过构造函数、DI 框架或提供者图从外部提供——而非内部创建
* \[ ] 注册区分生命周期:单例 vs 工厂 vs 惰性单例
* \[ ] 环境特定绑定(开发/暂存/生产)使用配置,而非运行时 `if` 检查
* \[ ] DI 图中没有循环依赖
* \[ ] 服务定位器调用(如果使用)没有散布在业务逻辑中
***
## 15. 静态分析
### 配置:
* \[ ] 存在 `analysis_options.yaml` 并启用了严格设置
* \[ ] 严格的分析器设置:`strict-casts: true``strict-inference: true``strict-raw-types: true`
* \[ ] 包含全面的 lint 规则集very\_good\_analysis、flutter\_lints 或自定义严格规则)
* \[ ] 单仓库中的所有子包继承或共享根分析选项
### 执行:
* \[ ] 提交的代码中没有未解决的分析器警告
* \[ ] lint 抑制(`// ignore:`)有注释说明原因
* \[ ] `flutter analyze` 在 CI 中运行,失败会阻止合并
### 无论使用何种 lint 包都要验证的关键规则:
* \[ ] `prefer_const_constructors`——小部件树中的性能
* \[ ] `avoid_print`——使用适当的日志记录
* \[ ] `unawaited_futures`——防止即发即弃的异步错误
* \[ ] `prefer_final_locals`——变量级别的不可变性
* \[ ] `always_declare_return_types`——明确的契约
* \[ ] `avoid_catches_without_on_clauses`——具体的错误处理
* \[ ] `always_use_package_imports`——一致的导入风格
***
## 状态管理快速参考
下表将通用原则映射到流行解决方案中的实现。使用此表将审查规则调整为项目使用的任何解决方案。
| 原则 | BLoC/Cubit | Riverpod | Provider | GetX | MobX | Signals | 内置 |
|-----------|-----------|----------|----------|------|------|---------|----------|
| 状态容器 | `Bloc`/`Cubit` | `Notifier`/`AsyncNotifier` | `ChangeNotifier` | `GetxController` | `Store` | `signal()` | `StatefulWidget` |
| UI 消费者 | `BlocBuilder` | `ConsumerWidget` | `Consumer` | `Obx`/`GetBuilder` | `Observer` | `Watch` | `setState` |
| 选择器 | `BlocSelector`/`buildWhen` | `ref.watch(p.select(...))` | `Selector` | N/A | computed | `computed()` | N/A |
| 副作用 | `BlocListener` | `ref.listen` | `Consumer` 回调 | `ever()`/`once()` | `reaction` | `effect()` | 回调 |
| 处置 | 通过 `BlocProvider` 自动 | `.autoDispose` | 通过 `Provider` 自动 | `onClose()` | `ReactionDisposer` | 手动 | `dispose()` |
| 测试 | `blocTest()` | `ProviderContainer` | 直接 `ChangeNotifier` | 在测试中 `Get.put` | 直接测试 store | 直接测试 signal | 小部件测试 |
***
## 来源
* [Effective Dart: 风格](https://dart.dev/effective-dart/style)
* [Effective Dart: 用法](https://dart.dev/effective-dart/usage)
* [Effective Dart: 设计](https://dart.dev/effective-dart/design)
* [Flutter 性能最佳实践](https://docs.flutter.dev/perf/best-practices)
* [Flutter 测试概述](https://docs.flutter.dev/testing/overview)
* [Flutter 无障碍功能](https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility)
* [Flutter 国际化](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization)
* [Flutter 导航和路由](https://docs.flutter.dev/ui/navigation)
* [Flutter 错误处理](https://docs.flutter.dev/testing/errors)
* [Flutter 状态管理选项](https://docs.flutter.dev/data-and-backend/state-mgmt/options)

View File

@@ -0,0 +1,415 @@
---
name: laravel-patterns
description: Laravel架构模式、路由/控制器、Eloquent ORM、服务层、队列、事件、缓存以及用于生产应用的API资源。
origin: ECC
---
# Laravel 开发模式
适用于可扩展、可维护应用的生产级 Laravel 架构模式。
## 适用场景
* 构建 Laravel Web 应用或 API
* 构建控制器、服务和领域逻辑
* 使用 Eloquent 模型和关系
* 使用资源和分页设计 API
* 添加队列、事件、缓存和后台任务
## 工作原理
* 围绕清晰的边界(控制器 -> 服务/操作 -> 模型)构建应用。
* 使用显式绑定和作用域绑定来保持路由可预测;同时仍强制执行授权以实现访问控制。
* 倾向于使用类型化模型、转换器和作用域来保持领域逻辑一致。
* 将 IO 密集型工作放在队列中,并缓存昂贵的读取操作。
* 将配置集中在 `config/*` 中,并保持环境配置显式化。
## 示例
### 项目结构
使用具有清晰层级边界HTTP、服务/操作、模型)的常规 Laravel 布局。
### 推荐布局
```
app/
├── Actions/ # Single-purpose use cases
├── Console/
├── Events/
├── Exceptions/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Requests/ # Form request validation
│ └── Resources/ # API resources
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/ # Coordinating domain services
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
```
### 控制器 -> 服务 -> 操作
保持控制器精简。将编排逻辑放在服务中,将单一职责逻辑放在操作中。
```php
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->orders->create($data);
}
}
final class OrdersController extends Controller
{
public function __construct(private CreateOrderAction $createOrder) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
```
### 路由与控制器
为了清晰起见,优先使用路由模型绑定和资源控制器。
```php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('projects', ProjectController::class);
});
```
### 路由模型绑定(作用域)
使用作用域绑定来防止跨租户访问。
```php
Route::scopeBindings()->group(function () {
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
```
### 嵌套路由和绑定名称
* 保持前缀和路径一致,避免双重嵌套(例如 `conversation``conversations`)。
* 使用与绑定模型匹配的单一参数名(例如,`{conversation}` 对应 `Conversation`)。
* 嵌套时优先使用作用域绑定以强制执行父子关系。
```php
use App\Http\Controllers\Api\ConversationController;
use App\Http\Controllers\Api\MessageController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () {
Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
Route::scopeBindings()->group(function () {
Route::get('/{conversation}', [ConversationController::class, 'show'])
->name('conversations.show');
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
->name('conversation-messages.store');
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
->name('conversation-messages.show');
});
});
```
如果希望参数解析为不同的模型类,请定义显式绑定。对于自定义绑定逻辑,请使用 `Route::bind()` 或在模型上实现 `resolveRouteBinding()`
```php
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;
Route::model('conversation', AiConversation::class);
```
### 服务容器绑定
在服务提供者中将接口绑定到实现,以实现清晰的依赖关系连接。
```php
use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
}
}
```
### Eloquent 模型模式
### 模型配置
```php
final class Project extends Model
{
use HasFactory;
protected $fillable = ['name', 'owner_id', 'status'];
protected $casts = [
'status' => ProjectStatus::class,
'archived_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
}
```
### 自定义转换器与值对象
使用枚举或值对象进行严格类型化。
```php
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];
```
```php
protected function budgetCents(): Attribute
{
return Attribute::make(
get: fn (int $value) => Money::fromCents($value),
set: fn (Money $money) => $money->toCents(),
);
}
```
### 预加载以避免 N+1 问题
```php
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);
```
### 用于复杂筛选的查询对象
```php
final class ProjectQuery
{
public function __construct(private Builder $query) {}
public function ownedBy(int $userId): self
{
$query = clone $this->query;
return new self($query->where('owner_id', $userId));
}
public function active(): self
{
$query = clone $this->query;
return new self($query->whereNull('archived_at'));
}
public function builder(): Builder
{
return $this->query;
}
}
```
### 全局作用域与软删除
使用全局作用域进行默认筛选,并使用 `SoftDeletes` 处理可恢复的记录。
对于同一筛选器,请使用全局作用域或命名作用域中的一种,除非你打算实现分层行为。
```php
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
use SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope('active', function (Builder $builder): void {
$builder->whereNull('archived_at');
});
}
}
```
### 用于可重用筛选器的查询作用域
```php
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
public function scopeOwnedBy(Builder $query, int $userId): Builder
{
return $query->where('owner_id', $userId);
}
}
// In service, repository etc.
$projects = Project::ownedBy($user->id)->get();
```
### 用于多步更新的数据库事务
```php
use Illuminate\Support\Facades\DB;
DB::transaction(function (): void {
$order->update(['status' => 'paid']);
$order->items()->update(['paid_at' => now()]);
});
```
### 数据库迁移
### 命名约定
* 文件名使用时间戳:`YYYY_MM_DD_HHMMSS_create_users_table.php`
* 迁移使用匿名类(无命名类);文件名传达意图
* 表名默认为 `snake_case` 且为复数形式
### 迁移示例
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('status', 32)->index();
$table->unsignedInteger('total_cents');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
```
### 表单请求与验证
将验证逻辑放在表单请求中,并将输入转换为 DTO。
```php
use App\Models\Order;
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Order::class) ?? false;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
public function toDto(): CreateOrderData
{
return new CreateOrderData(
customerId: (int) $this->validated('customer_id'),
items: $this->validated('items'),
);
}
}
```
### API 资源
使用资源和分页保持 API 响应一致。
```php
$projects = Project::query()->active()->paginate(25);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects->items()),
'error' => null,
'meta' => [
'page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
```
### 事件、任务和队列
* 为副作用(邮件、分析)触发领域事件
* 使用队列任务处理耗时工作报告、导出、Webhook
* 优先使用具有重试和退避机制的幂等处理器
### 缓存
* 缓存读密集型端点和昂贵查询
* 在模型事件(创建/更新/删除)时使缓存失效
* 缓存相关数据时使用标签以便于失效
### 配置与环境
* 将机密信息保存在 `.env` 中,将配置保存在 `config/*.php`
* 使用按环境配置覆盖,并在生产环境中使用 `config:cache`

View File

@@ -0,0 +1,285 @@
---
name: laravel-security
description: Laravel 安全最佳实践,涵盖认证/授权、验证、CSRF、批量赋值、文件上传、密钥管理、速率限制和安全部署。
origin: ECC
---
# Laravel 安全最佳实践
针对 Laravel 应用程序的全面安全指导,以防范常见漏洞。
## 何时启用
* 添加身份验证或授权时
* 处理用户输入和文件上传时
* 构建新的 API 端点时
* 管理密钥和环境设置时
* 强化生产环境部署时
## 工作原理
* 中间件提供基础保护(通过 `VerifyCsrfToken` 实现 CSRF通过 `SecurityHeaders` 实现安全标头)。
* 守卫和策略强制执行访问控制(`auth:sanctum``$this->authorize`、策略中间件)。
* 表单请求在输入到达服务之前进行验证和整形(`UploadInvoiceRequest`)。
* 速率限制在身份验证控制之外增加滥用保护(`RateLimiter::for('login')`)。
* 数据安全来自加密转换、批量赋值保护以及签名路由(`URL::temporarySignedRoute` + `signed` 中间件)。
## 核心安全设置
* 生产环境中设置 `APP_DEBUG=false`
* `APP_KEY` 必须设置,并在泄露时轮换
* 设置 `SESSION_SECURE_COOKIE=true``SESSION_SAME_SITE=lax`(对于敏感应用,使用 `strict`
* 配置受信任的代理以正确检测 HTTPS
## 会话和 Cookie 强化
* 设置 `SESSION_HTTP_ONLY=true` 以防止 JavaScript 访问
* 对高风险流程使用 `SESSION_SAME_SITE=strict`
* 在登录和权限变更时重新生成会话
## 身份验证与令牌
* 使用 Laravel Sanctum 或 Passport 进行 API 身份验证
* 对于敏感数据,优先使用带有刷新流程的短期令牌
* 在注销和账户泄露时撤销令牌
路由保护示例:
```php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->get('/me', function (Request $request) {
return $request->user();
});
```
## 密码安全
* 使用 `Hash::make()` 哈希密码,切勿存储明文
* 使用 Laravel 的密码代理进行重置流程
```php
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
$validated = $request->validate([
'password' => ['required', 'string', Password::min(12)->letters()->mixedCase()->numbers()->symbols()],
]);
$user->update(['password' => Hash::make($validated['password'])]);
```
## 授权:策略与门面
* 使用策略进行模型级授权
* 在控制器和服务中强制执行授权
```php
$this->authorize('update', $project);
```
使用策略中间件进行路由级强制执行:
```php
use Illuminate\Support\Facades\Route;
Route::put('/projects/{project}', [ProjectController::class, 'update'])
->middleware(['auth:sanctum', 'can:update,project']);
```
## 验证与数据清理
* 始终使用表单请求验证输入
* 使用严格的验证规则和类型检查
* 切勿信任请求负载中的派生字段
## 批量赋值保护
* 使用 `$fillable``$guarded`,避免使用 `Model::unguard()`
* 优先使用 DTO 或显式的属性映射
## SQL 注入防范
* 使用 Eloquent 或查询构建器的参数绑定
* 除非绝对必要,避免使用原生 SQL
```php
DB::select('select * from users where email = ?', [$email]);
```
## XSS 防范
* Blade 默认转义输出(`{{ }}`
* 仅对可信的、已清理的 HTML 使用 `{!! !!}`
* 使用专用库清理富文本
## CSRF 保护
* 保持 `VerifyCsrfToken` 中间件启用
* 在表单中包含 `@csrf`,并为 SPA 请求发送 XSRF 令牌
对于使用 Sanctum 的 SPA 身份验证,确保配置了有状态请求:
```php
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),
```
## 文件上传安全
* 验证文件大小、MIME 类型和扩展名
* 尽可能将上传文件存储在公开路径之外
* 如果需要,扫描文件以查找恶意软件
```php
final class UploadInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return (bool) $this->user()?->can('upload-invoice');
}
public function rules(): array
{
return [
'invoice' => ['required', 'file', 'mimes:pdf', 'max:5120'],
];
}
}
```
```php
$path = $request->file('invoice')->store(
'invoices',
config('filesystems.private_disk', 'local') // set this to a non-public disk
);
```
## 速率限制
* 在身份验证和写入端点应用 `throttle` 中间件
* 对登录、密码重置和 OTP 使用更严格的限制
```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perMinute(5)->by(strtolower((string) $request->input('email'))),
];
});
```
## 密钥与凭据
* 切勿将密钥提交到源代码管理
* 使用环境变量和密钥管理器
* 密钥暴露后及时轮换,并使会话失效
## 加密属性
对静态的敏感列使用加密转换。
```php
protected $casts = [
'api_token' => 'encrypted',
];
```
## 安全标头
* 在适当的地方添加 CSP、HSTS 和框架保护
* 使用受信任的代理配置来强制执行 HTTPS 重定向
设置标头的中间件示例:
```php
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class SecurityHeaders
{
public function handle(Request $request, \Closure $next): Response
{
$response = $next($request);
$response->headers->add([
'Content-Security-Policy' => "default-src 'self'",
'Strict-Transport-Security' => 'max-age=31536000', // add includeSubDomains/preload only when all subdomains are HTTPS
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'no-referrer',
]);
return $response;
}
}
```
## CORS 与 API 暴露
*`config/cors.php` 中限制来源
* 对于经过身份验证的路由,避免使用通配符来源
```php
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'allowed_origins' => ['https://app.example.com'],
'allowed_headers' => [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-XSRF-TOKEN',
'X-CSRF-TOKEN',
],
'supports_credentials' => true,
];
```
## 日志记录与 PII
* 切勿记录密码、令牌或完整的卡片数据
* 在结构化日志中编辑敏感字段
```php
use Illuminate\Support\Facades\Log;
Log::info('User updated profile', [
'user_id' => $user->id,
'email' => '[REDACTED]',
'token' => '[REDACTED]',
]);
```
## 依赖项安全
* 定期运行 `composer audit`
* 谨慎固定依赖项版本,并在出现 CVE 时及时更新
## 签名 URL
使用签名路由生成临时的、防篡改的链接。
```php
use Illuminate\Support\Facades\URL;
$url = URL::temporarySignedRoute(
'downloads.invoice',
now()->addMinutes(15),
['invoice' => $invoice->id]
);
```
```php
use Illuminate\Support\Facades\Route;
Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download'])
->name('downloads.invoice')
->middleware('signed');
```

View File

@@ -0,0 +1,283 @@
---
name: laravel-tdd
description: 使用 PHPUnit 和 Pest、工厂、数据库测试、模拟以及覆盖率目标进行 Laravel 的测试驱动开发。
origin: ECC
---
# Laravel TDD 工作流
使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发,覆盖率(单元 + 功能)达到 80% 以上。
## 使用时机
* Laravel 中的新功能或端点
* 错误修复或重构
* 测试 Eloquent 模型、策略、作业和通知
* 除非项目已标准化使用 PHPUnit否则新测试首选 Pest
## 工作原理
### 红-绿-重构循环
1. 编写一个失败的测试
2. 实施最小更改以通过测试
3. 在保持测试通过的同时进行重构
### 测试层级
* **单元**:纯 PHP 类、值对象、服务
* **功能**HTTP 端点、身份验证、验证、策略
* **集成**:数据库 + 队列 + 外部边界
根据范围选择层级:
* 对纯业务逻辑和服务使用**单元**测试。
* 对 HTTP、身份验证、验证和响应结构使用**功能**测试。
* 当需要验证数据库/队列/外部服务组合时使用**集成**测试。
### 数据库策略
* 对于大多数功能/集成测试使用 `RefreshDatabase`(每次测试运行运行一次迁移,然后在支持时将每个测试包装在事务中;内存数据库可能每次测试重新迁移)
* 当模式已迁移且仅需要每次测试回滚时使用 `DatabaseTransactions`
* 当每次测试都需要完整迁移/刷新且可以承担其开销时使用 `DatabaseMigrations`
`RefreshDatabase` 作为触及数据库的测试的默认选择:对于支持事务的数据库,它每次测试运行运行一次迁移(通过静态标志)并将每个测试包装在事务中;对于 `:memory:` SQLite 或不支持事务的连接,它在每次测试前进行迁移。当模式已迁移且仅需要每次测试回滚时使用 `DatabaseTransactions`
### 测试框架选择
* 新测试默认使用 **Pest**(当可用时)。
* 仅在项目已标准化使用它或需要 PHPUnit 特定工具时使用 **PHPUnit**
## 示例
### PHPUnit 示例
```php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectControllerTest extends TestCase
{
use RefreshDatabase;
public function test_owner_can_create_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
$this->assertDatabaseHas('projects', ['name' => 'New Project']);
}
}
```
### 功能测试示例HTTP 层)
```php
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectIndexTest extends TestCase
{
use RefreshDatabase;
public function test_projects_index_returns_paginated_results(): void
{
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = $this->actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
}
}
```
### Pest 示例
```php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
uses(RefreshDatabase::class);
test('owner can create project', function () {
$user = User::factory()->create();
$response = actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
assertDatabaseHas('projects', ['name' => 'New Project']);
});
```
### Pest 功能测试示例HTTP 层)
```php
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
test('projects index returns paginated results', function () {
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
});
```
### 工厂和状态
* 使用工厂生成测试数据
* 为边缘情况定义状态(已归档、管理员、试用)
```php
$user = User::factory()->state(['role' => 'admin'])->create();
```
### 数据库测试
* 使用 `RefreshDatabase` 保持干净状态
* 保持测试隔离和确定性
* 优先使用 `assertDatabaseHas` 而非手动查询
### 持久性测试示例
```php
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectRepositoryTest extends TestCase
{
use RefreshDatabase;
public function test_project_can_be_retrieved_by_slug(): void
{
$project = Project::factory()->create(['slug' => 'alpha']);
$found = Project::query()->where('slug', 'alpha')->firstOrFail();
$this->assertSame($project->id, $found->id);
}
}
```
### 副作用模拟
* 作业使用 `Bus::fake()`
* 队列工作使用 `Queue::fake()`
* 通知使用 `Mail::fake()``Notification::fake()`
* 领域事件使用 `Event::fake()`
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
dispatch(new SendOrderConfirmation($order->id));
Queue::assertPushed(SendOrderConfirmation::class);
```
```php
use Illuminate\Support\Facades\Notification;
Notification::fake();
$user->notify(new InvoiceReady($invoice));
Notification::assertSentTo($user, InvoiceReady::class);
```
### 身份验证测试Sanctum
```php
use Laravel\Sanctum\Sanctum;
Sanctum::actingAs($user);
$response = $this->getJson('/api/projects');
$response->assertOk();
```
### HTTP 和外部服务
* 使用 `Http::fake()` 隔离外部 API
* 使用 `Http::assertSent()` 断言出站负载
### 覆盖率目标
* 对单元 + 功能测试强制执行 80% 以上的覆盖率
* 在 CI 中使用 `pcov``XDEBUG_MODE=coverage`
### 测试命令
* `php artisan test`
* `vendor/bin/phpunit`
* `vendor/bin/pest`
### 测试配置
* 使用 `phpunit.xml` 设置 `DB_CONNECTION=sqlite``DB_DATABASE=:memory:` 以进行快速测试
* 为测试保持独立的环境,以避免触及开发/生产数据
### 授权测试
```php
use Illuminate\Support\Facades\Gate;
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
```
### Inertia 功能测试
使用 Inertia.js 时,使用 Inertia 测试辅助函数来断言组件名称和属性。
```php
use App\Models\User;
use Inertia\Testing\AssertableInertia;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class DashboardInertiaTest extends TestCase
{
use RefreshDatabase;
public function test_dashboard_inertia_props(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('Dashboard')
->where('user.id', $user->id)
->has('projects')
);
}
}
```
优先使用 `assertInertia` 而非原始 JSON 断言,以保持测试与 Inertia 响应一致。

View File

@@ -0,0 +1,179 @@
---
name: laravel-verification
description: Verification loop for Laravel projects: env checks, linting, static analysis, tests with coverage, security scans, and deployment readiness.
origin: ECC
---
# Laravel 验证循环
在发起 PR 前、进行重大更改后以及部署前运行。
## 使用时机
* 在为一个 Laravel 项目开启拉取请求之前
* 在重大重构或依赖升级之后
* 为预生产或生产环境进行部署前验证
* 运行完整的 代码检查 -> 测试 -> 安全检查 -> 部署就绪 流水线
## 工作原理
* 按顺序运行从环境检查到部署就绪的各个阶段,每一层都建立在前一层的基础上。
* 环境和 Composer 检查是所有其他步骤的关卡;如果它们失败,立即停止。
* 代码检查/静态分析应在运行完整测试和覆盖率检查前确保通过。
* 安全性和迁移审查在测试之后进行,以便在涉及数据或发布步骤之前验证行为。
* 构建/部署就绪以及队列/调度器检查是最后的关卡;任何失败都会阻止发布。
## 第一阶段:环境检查
```bash
php -v
composer --version
php artisan --version
```
* 验证 `.env` 文件存在且包含必需的键
* 确认生产环境已设置 `APP_DEBUG=false`
* 确认 `APP_ENV` 与目标部署环境匹配(`production``staging`
如果在本地使用 Laravel Sail
```bash
./vendor/bin/sail php -v
./vendor/bin/sail artisan --version
```
## 第一阶段补充Composer 和自动加载
```bash
composer validate
composer dump-autoload -o
```
## 第二阶段:代码检查和静态分析
```bash
vendor/bin/pint --test
vendor/bin/phpstan analyse
```
如果你的项目使用 Psalm 而不是 PHPStan
```bash
vendor/bin/psalm
```
## 第三阶段:测试和覆盖率
```bash
php artisan test
```
覆盖率CI 环境):
```bash
XDEBUG_MODE=coverage php artisan test --coverage
```
CI 示例(格式化 -> 静态分析 -> 测试):
```bash
vendor/bin/pint --test
vendor/bin/phpstan analyse
XDEBUG_MODE=coverage php artisan test --coverage
```
## 第四阶段:安全和依赖项检查
```bash
composer audit
```
## 第五阶段:数据库和迁移
```bash
php artisan migrate --pretend
php artisan migrate:status
```
* 仔细审查破坏性迁移
* 确保迁移文件名遵循 `Y_m_d_His_*` 格式(例如,`2025_03_14_154210_create_orders_table.php`)并清晰地描述变更
* 确保可以执行回滚
* 验证 `down()` 方法,避免在没有明确备份的情况下造成不可逆的数据丢失
## 第六阶段:构建和部署就绪
```bash
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
* 确保在生产配置下缓存预热成功
* 验证队列工作者和调度器已配置
* 确认在目标环境中 `storage/``bootstrap/cache/` 目录可写
## 第七阶段:队列和调度器检查
```bash
php artisan schedule:list
php artisan queue:failed
```
如果使用了 Horizon
```bash
php artisan horizon:status
```
如果 `queue:monitor` 命令可用,可以用它来检查积压作业而无需处理它们:
```bash
php artisan queue:monitor default --max=100
```
主动验证(仅限预生产环境):向一个专用队列分发一个无操作作业,并运行一个单独的工作者来处理它(确保配置了一个非 `sync` 的队列连接)。
```bash
php artisan tinker --execute="dispatch((new App\\Jobs\\QueueHealthcheck())->onQueue('healthcheck'))"
php artisan queue:work --once --queue=healthcheck
```
验证该作业产生了预期的副作用(日志条目、健康检查表行或指标)。
仅在处理测试作业是安全的非生产环境中运行此检查。
## 示例
最小流程:
```bash
php -v
composer --version
php artisan --version
composer validate
vendor/bin/pint --test
vendor/bin/phpstan analyse
php artisan test
composer audit
php artisan migrate --pretend
php artisan config:cache
php artisan queue:failed
```
CI 风格流水线:
```bash
composer validate
composer dump-autoload -o
vendor/bin/pint --test
vendor/bin/phpstan analyse
XDEBUG_MODE=coverage php artisan test --coverage
composer audit
php artisan migrate --pretend
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan schedule:list
```

View File

@@ -0,0 +1,67 @@
---
name: mcp-server-patterns
description: 使用Node/TypeScript SDK构建MCP服务器——工具、资源、提示、Zod验证、stdio与可流式HTTP对比。使用Context7或官方MCP文档获取最新API信息。
origin: ECC
---
# MCP 服务器模式
模型上下文协议MCP允许 AI 助手调用工具、读取资源和使用来自服务器的提示。在构建或维护 MCP 服务器时使用此技能。SDK API 会演进;请查阅 Context7查询文档 "MCP")或官方 MCP 文档以获取当前的方法名称和签名。
## 何时使用
在以下情况时使用:实现新的 MCP 服务器、添加工具或资源、选择 stdio 与 HTTP、升级 SDK或调试 MCP 注册和传输问题。
## 工作原理
### 核心概念
* **工具**:模型可以调用的操作(例如搜索、运行命令)。根据 SDK 版本,使用 `registerTool()``tool()` 注册。
* **资源**模型可以获取的只读数据例如文件内容、API 响应)。根据 SDK 版本,使用 `registerResource()``resource()` 注册。处理程序通常接收一个 `uri` 参数。
* **提示**:客户端可以呈现的可重用参数化提示模板(例如在 Claude Desktop 中)。使用 `registerPrompt()` 或等效方法注册。
* **传输**stdio 用于本地客户端(例如 Claude Desktop可流式 HTTP 是远程Cursor、云端的首选。传统 HTTP/SSE 用于向后兼容。
Node/TypeScript SDK 可能暴露 `tool()` / `resource()``registerTool()` / `registerResource()`;官方 SDK 已随时间变化。请始终根据当前 [MCP 文档](https://modelcontextprotocol.io) 或 Context7 进行验证。
### 使用 stdio 连接
对于本地客户端,创建一个 stdio 传输并将其传递给服务器的连接方法。确切的 API 因 SDK 版本而异(例如构造函数与工厂函数)。请参阅官方 MCP 文档或查询 Context7 中的 "MCP stdio server" 以获取当前模式。
保持服务器逻辑(工具 + 资源)独立于传输,以便您可以在入口点中插入 stdio 或 HTTP。
### 远程(可流式 HTTP
对于 Cursor、云端或其他远程客户端使用**可流式 HTTP**(根据当前规范,每个 MCP HTTP 端点)。仅在需要向后兼容性时支持传统 HTTP/SSE。
## 示例
### 安装和服务器设置
```bash
npm install @modelcontextprotocol/sdk zod
```
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
```
使用您的 SDK 版本提供的 API 注册工具和资源:某些版本使用 `server.tool(name, description, schema, handler)`(位置参数),其他版本使用 `server.tool({ name, description, inputSchema }, handler)``registerTool()`。资源同理——当 API 提供时,在处理程序中包含一个 `uri`。请查阅官方 MCP 文档或 Context7 以获取当前的 `@modelcontextprotocol/sdk` 签名,避免复制粘贴错误。
使用 **Zod**(或 SDK 首选的模式格式)进行输入验证。
## 最佳实践
* **模式优先**:为每个工具定义输入模式;记录参数和返回形状。
* **错误处理**:返回结构化错误或模型可以解释的消息;避免原始堆栈跟踪。
* **幂等性**:尽可能使用幂等工具,以便重试是安全的。
* **速率和成本**:对于调用外部 API 的工具,请考虑速率限制和成本;在工具描述中加以说明。
* **版本控制**:在 package.json 中固定 SDK 版本;升级时查看发行说明。
## 官方 SDK 和文档
* **JavaScript/TypeScript**`@modelcontextprotocol/sdk` (npm)。使用库名 "MCP" 的 Context7 以获取当前的注册和传输模式。
* **Go**GitHub 上的官方 Go SDK (`modelcontextprotocol/go-sdk`)。
* **C#**:适用于 .NET 的官方 C# SDK。

View File

@@ -0,0 +1,44 @@
---
name: nextjs-turbopack
description: Next.js 16+ 和 Turbopack — 增量打包、文件系统缓存、开发速度,以及何时使用 Turbopack 与 webpack。
origin: ECC
---
# Next.js 与 Turbopack
Next.js 16+ 在本地开发中默认使用 Turbopack这是一个用 Rust 编写的增量捆绑器,能显著加快开发启动和热更新的速度。
## 何时使用
* **Turbopack (默认开发模式)**:用于日常开发。冷启动和热模块替换速度更快,尤其是在大型应用中。
* **Webpack (旧版开发模式)**:仅当遇到 Turbopack 错误或依赖仅在开发中可用的 webpack 插件时使用。可通过 `--webpack`(或 `--no-turbopack`,具体取决于你的 Next.js 版本;请查阅你所用版本的文档)来禁用。
* **生产环境**:生产构建行为 (`next build`) 可能使用 Turbopack 或 webpack这取决于 Next.js 版本;请查阅你所用版本的官方 Next.js 文档。
适用场景:开发或调试 Next.js 16+ 应用,诊断开发启动或热模块替换速度慢的问题,或优化生产环境捆绑包。
## 工作原理
* **Turbopack**:用于 Next.js 开发的增量捆绑器。利用文件系统缓存,因此重启速度要快得多(例如,在大型项目中快 514 倍)。
* **开发环境默认启用**:从 Next.js 16 开始,`next dev` 默认使用 Turbopack除非被禁用。
* **文件系统缓存**:重启时会复用之前的工作成果;缓存通常位于 `.next` 下;基本使用无需额外配置。
* **捆绑包分析器 (Next.js 16.1+)**:实验性的捆绑包分析器,用于检查输出并发现重型依赖;可通过配置或实验性标志启用(请查阅你所用版本的 Next.js 文档)。
## 示例
### 命令
```bash
next dev
next build
next start
```
### 使用
运行 `next dev` 以使用 Turbopack 进行本地开发。使用捆绑包分析器(参见 Next.js 文档)来优化代码分割并剔除大型依赖。尽可能优先使用 App Router 和服务器组件。
## 最佳实践
* 保持使用较新的 Next.js 16.x 版本,以获得稳定的 Turbopack 和缓存行为。
* 如果开发速度慢,请确保你正在使用 Turbopack默认并且缓存没有被不必要地清除。
* 对于生产环境捆绑包大小问题,请使用你所用版本的官方 Next.js 捆绑包分析工具。

View File

@@ -0,0 +1,100 @@
---
name: nuxt4-patterns
description: Nuxt 4 应用模式,涵盖水合安全、性能优化、路由规则、懒加载,以及使用 useFetch 和 useAsyncData 进行 SSR 安全的数据获取。
origin: ECC
---
# Nuxt 4 模式
在构建或调试具有 SSR、混合渲染、路由规则或页面级数据获取的 Nuxt 4 应用时使用。
## 何时激活
* 服务器 HTML 与客户端状态之间的水合不匹配
* 路由级别的渲染决策例如预渲染、SWR、ISR 或仅客户端部分
* 围绕懒加载、延迟水合或有效负载大小的性能工作
* 使用 `useFetch``useAsyncData``$fetch` 进行页面或组件数据获取
* 与路由参数、中间件或 SSR/客户端差异相关的 Nuxt 路由问题
## 水合安全性
* 保持首次渲染是确定性的。不要将 `Date.now()``Math.random()`、仅限浏览器的 API 或存储读取直接放入 SSR 渲染的模板状态中。
* 当服务器无法生成相同标记时,将仅限浏览器的逻辑移到 `onMounted()``import.meta.client``ClientOnly``.client.vue` 组件后面。
* 使用 Nuxt 的 `useRoute()` 组合式函数,而不是来自 `vue-router` 的那个。
* 不要使用 `route.fullPath` 来驱动 SSR 渲染的标记。URL 片段是仅客户端的,这可能导致水合不匹配。
*`ssr: false` 视为真正仅限浏览器区域的逃生舱口,而不是解决不匹配的默认修复方法。
## 数据获取
* 在页面和组件中,优先使用 `await useFetch()` 进行 SSR 安全的 API 读取。它将服务器获取的数据转发到 Nuxt 有效负载中,并避免在水合时进行第二次获取。
* 当数据获取器不是简单的 `$fetch()` 调用,或者需要自定义键,或者正在组合多个异步源时,使用 `useAsyncData()`
*`useAsyncData()` 提供一个稳定的键以重用缓存并实现可预测的刷新行为。
* 保持 `useAsyncData()` 处理程序无副作用。它们可能在 SSR 和水合期间运行。
*`$fetch()` 用于用户触发的写入或仅客户端操作,而不是应该从 SSR 水合而来的顶级页面数据。
* 对于不应阻塞导航的非关键数据,使用 `lazy: true``useLazyFetch()``useLazyAsyncData()`。在 UI 中处理 `status === 'pending'`
* 仅对 SEO 或首次绘制不需要的数据使用 `server: false`
* 使用 `pick` 修剪有效负载大小,并在不需要深层响应性时优先使用较浅的有效负载。
```ts
const route = useRoute()
const { data: article, status, error, refresh } = await useAsyncData(
() => `article:${route.params.slug}`,
() => $fetch(`/api/articles/${route.params.slug}`),
)
const { data: comments } = await useFetch(`/api/articles/${route.params.slug}/comments`, {
lazy: true,
server: false,
})
```
## 路由规则
`nuxt.config.ts` 中优先使用 `routeRules` 来定义渲染和缓存策略:
```ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/products/**': { swr: 3600 },
'/blog/**': { isr: true },
'/admin/**': { ssr: false },
'/api/**': { cache: { maxAge: 60 * 60 } },
},
})
```
* `prerender`:在构建时生成静态 HTML
* `swr`:提供缓存内容并在后台重新验证
* `isr`:在支持的平台上进行增量静态再生
* `ssr: false`:客户端渲染的路由
* `cache``redirect`Nitro 级别的响应行为
按路由组选择路由规则,而非全局设置。营销页面、产品目录、仪表板和 API 通常需要不同的策略。
## 懒加载与性能
* Nuxt 已经按路由进行代码分割。在微优化组件分割之前,保持路由边界的意义。
* 使用 `Lazy` 前缀来动态导入非关键组件。
* 使用 `v-if` 有条件地渲染懒加载组件,以便在 UI 实际需要时才加载该代码块。
* 对首屏下方或非关键的交互式 UI 使用延迟水合。
```vue
<template>
<LazyRecommendations v-if="showRecommendations" />
<LazyProductGallery hydrate-on-visible />
</template>
```
* 对于自定义策略,使用 `defineLazyHydrationComponent()` 配合可见性或空闲策略。
* Nuxt 延迟水合适用于单文件组件。向延迟水合的组件传递新 props 将立即触发水合。
* 在内部导航中使用 `NuxtLink`,以便 Nuxt 可以预取路由组件和生成的有效负载。
## 检查清单
* 首次 SSR 渲染和水合后的客户端渲染产生相同的标记
* 页面数据使用 `useFetch``useAsyncData`,而非顶层的 `$fetch`
* 非关键数据是懒加载的,并具有明确的加载 UI
* 路由规则符合页面的 SEO 和新鲜度要求
* 重量级交互式组件是懒加载或延迟水合的

View File

@@ -0,0 +1,396 @@
---
name: pytorch-patterns
description: PyTorch深度学习模式与最佳实践用于构建稳健、高效且可复现的训练流程、模型架构和数据加载。
origin: ECC
---
# PyTorch 开发模式
构建稳健、高效和可复现深度学习应用的 PyTorch 惯用模式与最佳实践。
## 何时使用
* 编写新的 PyTorch 模型或训练脚本时
* 评审深度学习代码时
* 调试训练循环或数据管道时
* 优化 GPU 内存使用或训练速度时
* 设置可复现实验时
## 核心原则
### 1. 设备无关代码
始终编写能在 CPU 和 GPU 上运行且不硬编码设备的代码。
```python
# Good: Device-agnostic
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
data = data.to(device)
# Bad: Hardcoded device
model = MyModel().cuda() # Crashes if no GPU
data = data.cuda()
```
### 2. 可复现性优先
设置所有随机种子以获得可复现的结果。
```python
# Good: Full reproducibility setup
def set_seed(seed: int = 42) -> None:
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# Bad: No seed control
model = MyModel() # Different weights every run
```
### 3. 显式形状管理
始终记录并验证张量形状。
```python
# Good: Shape-annotated forward pass
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (batch_size, channels, height, width)
x = self.conv1(x) # -> (batch_size, 32, H, W)
x = self.pool(x) # -> (batch_size, 32, H//2, W//2)
x = x.view(x.size(0), -1) # -> (batch_size, 32*H//2*W//2)
return self.fc(x) # -> (batch_size, num_classes)
# Bad: No shape tracking
def forward(self, x):
x = self.conv1(x)
x = self.pool(x)
x = x.view(x.size(0), -1) # What size is this?
return self.fc(x) # Will this even work?
```
## 模型架构模式
### 清晰的 nn.Module 结构
```python
# Good: Well-organized module
class ImageClassifier(nn.Module):
def __init__(self, num_classes: int, dropout: float = 0.5) -> None:
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(64 * 16 * 16, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
# Bad: Everything in forward
class ImageClassifier(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
x = F.conv2d(x, weight=self.make_weight()) # Creates weight each call!
return x
```
### 正确的权重初始化
```python
# Good: Explicit initialization
def _init_weights(self, module: nn.Module) -> None:
if isinstance(module, nn.Linear):
nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Conv2d):
nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
elif isinstance(module, nn.BatchNorm2d):
nn.init.ones_(module.weight)
nn.init.zeros_(module.bias)
model = MyModel()
model.apply(model._init_weights)
```
## 训练循环模式
### 标准训练循环
```python
# Good: Complete training loop with best practices
def train_one_epoch(
model: nn.Module,
dataloader: DataLoader,
optimizer: torch.optim.Optimizer,
criterion: nn.Module,
device: torch.device,
scaler: torch.amp.GradScaler | None = None,
) -> float:
model.train() # Always set train mode
total_loss = 0.0
for batch_idx, (data, target) in enumerate(dataloader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad(set_to_none=True) # More efficient than zero_grad()
# Mixed precision training
with torch.amp.autocast("cuda", enabled=scaler is not None):
output = model(data)
loss = criterion(output, target)
if scaler is not None:
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()
else:
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
```
### 验证循环
```python
# Good: Proper evaluation
@torch.no_grad() # More efficient than wrapping in torch.no_grad() block
def evaluate(
model: nn.Module,
dataloader: DataLoader,
criterion: nn.Module,
device: torch.device,
) -> tuple[float, float]:
model.eval() # Always set eval mode — disables dropout, uses running BN stats
total_loss = 0.0
correct = 0
total = 0
for data, target in dataloader:
data, target = data.to(device), target.to(device)
output = model(data)
total_loss += criterion(output, target).item()
correct += (output.argmax(1) == target).sum().item()
total += target.size(0)
return total_loss / len(dataloader), correct / total
```
## 数据管道模式
### 自定义数据集
```python
# Good: Clean Dataset with type hints
class ImageDataset(Dataset):
def __init__(
self,
image_dir: str,
labels: dict[str, int],
transform: transforms.Compose | None = None,
) -> None:
self.image_paths = list(Path(image_dir).glob("*.jpg"))
self.labels = labels
self.transform = transform
def __len__(self) -> int:
return len(self.image_paths)
def __getitem__(self, idx: int) -> tuple[torch.Tensor, int]:
img = Image.open(self.image_paths[idx]).convert("RGB")
label = self.labels[self.image_paths[idx].stem]
if self.transform:
img = self.transform(img)
return img, label
```
### 高效的数据加载器配置
```python
# Good: Optimized DataLoader
dataloader = DataLoader(
dataset,
batch_size=32,
shuffle=True, # Shuffle for training
num_workers=4, # Parallel data loading
pin_memory=True, # Faster CPU->GPU transfer
persistent_workers=True, # Keep workers alive between epochs
drop_last=True, # Consistent batch sizes for BatchNorm
)
# Bad: Slow defaults
dataloader = DataLoader(dataset, batch_size=32) # num_workers=0, no pin_memory
```
### 针对变长数据的自定义整理函数
```python
# Good: Pad sequences in collate_fn
def collate_fn(batch: list[tuple[torch.Tensor, int]]) -> tuple[torch.Tensor, torch.Tensor]:
sequences, labels = zip(*batch)
# Pad to max length in batch
padded = nn.utils.rnn.pad_sequence(sequences, batch_first=True, padding_value=0)
return padded, torch.tensor(labels)
dataloader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn)
```
## 检查点模式
### 保存和加载检查点
```python
# Good: Complete checkpoint with all training state
def save_checkpoint(
model: nn.Module,
optimizer: torch.optim.Optimizer,
epoch: int,
loss: float,
path: str,
) -> None:
torch.save({
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"loss": loss,
}, path)
def load_checkpoint(
path: str,
model: nn.Module,
optimizer: torch.optim.Optimizer | None = None,
) -> dict:
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
model.load_state_dict(checkpoint["model_state_dict"])
if optimizer:
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
return checkpoint
# Bad: Only saving model weights (can't resume training)
torch.save(model.state_dict(), "model.pt")
```
## 性能优化
### 混合精度训练
```python
# Good: AMP with GradScaler
scaler = torch.amp.GradScaler("cuda")
for data, target in dataloader:
with torch.amp.autocast("cuda"):
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)
```
### 大模型的梯度检查点
```python
# Good: Trade compute for memory
from torch.utils.checkpoint import checkpoint
class LargeModel(nn.Module):
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Recompute activations during backward to save memory
x = checkpoint(self.block1, x, use_reentrant=False)
x = checkpoint(self.block2, x, use_reentrant=False)
return self.head(x)
```
### 使用 torch.compile 加速
```python
# Good: Compile the model for faster execution (PyTorch 2.0+)
model = MyModel().to(device)
model = torch.compile(model, mode="reduce-overhead")
# Modes: "default" (safe), "reduce-overhead" (faster), "max-autotune" (fastest)
```
## 快速参考PyTorch 惯用法
| 惯用法 | 描述 |
|-------|-------------|
| `model.train()` / `model.eval()` | 训练/评估前始终设置模式 |
| `torch.no_grad()` | 推理时禁用梯度 |
| `optimizer.zero_grad(set_to_none=True)` | 更高效的梯度清零 |
| `.to(device)` | 设备无关的张量/模型放置 |
| `torch.amp.autocast` | 混合精度以获得 2 倍速度 |
| `pin_memory=True` | 更快的 CPU→GPU 数据传输 |
| `torch.compile` | JIT 编译加速 (2.0+) |
| `weights_only=True` | 安全的模型加载 |
| `torch.manual_seed` | 可复现的实验 |
| `gradient_checkpointing` | 以计算换取内存 |
## 应避免的反模式
```python
# Bad: Forgetting model.eval() during validation
model.train()
with torch.no_grad():
output = model(val_data) # Dropout still active! BatchNorm uses batch stats!
# Good: Always set eval mode
model.eval()
with torch.no_grad():
output = model(val_data)
# Bad: In-place operations breaking autograd
x = F.relu(x, inplace=True) # Can break gradient computation
x += residual # In-place add breaks autograd graph
# Good: Out-of-place operations
x = F.relu(x)
x = x + residual
# Bad: Moving data to GPU inside the training loop repeatedly
for data, target in dataloader:
model = model.cuda() # Moves model EVERY iteration!
# Good: Move model once before the loop
model = model.to(device)
for data, target in dataloader:
data, target = data.to(device), target.to(device)
# Bad: Using .item() before backward
loss = criterion(output, target).item() # Detaches from graph!
loss.backward() # Error: can't backprop through .item()
# Good: Call .item() only for logging
loss = criterion(output, target)
loss.backward()
print(f"Loss: {loss.item():.4f}") # .item() after backward is fine
# Bad: Not using torch.save properly
torch.save(model, "model.pt") # Saves entire model (fragile, not portable)
# Good: Save state_dict
torch.save(model.state_dict(), "model.pt")
```
**请记住**PyTorch 代码应做到设备无关、可复现且内存意识强。如有疑问,请使用 `torch.profiler` 进行分析,并使用 `torch.cuda.memory_summary()` 检查 GPU 内存。

View File

@@ -0,0 +1,266 @@
---
name: rules-distill
description: "扫描技能以提取跨领域原则并将其提炼为规则——追加、修订或创建新的规则文件"
origin: ECC
---
# 规则提炼
扫描已安装的技能,提取在多个技能中出现的通用原则,并将其提炼成规则——追加到现有规则文件中、修订过时内容或创建新的规则文件。
应用"确定性收集 + LLM判断"原则脚本详尽地收集事实然后由LLM通读完整上下文并作出裁决。
## 使用时机
* 定期规则维护(每月或安装新技能后)
* 技能盘点后,发现应成为规则的模式时
* 当规则相对于正在使用的技能感觉不完整时
## 工作原理
规则提炼过程遵循三个阶段:
### 阶段 1清点确定性收集
#### 1a. 收集技能清单
```bash
bash ~/.claude/skills/rules-distill/scripts/scan-skills.sh
```
#### 1b. 收集规则索引
```bash
bash ~/.claude/skills/rules-distill/scripts/scan-rules.sh
```
#### 1c. 呈现给用户
```
Rules Distillation — Phase 1: Inventory
────────────────────────────────────────
Skills: {N} files scanned
Rules: {M} files ({K} headings indexed)
Proceeding to cross-read analysis...
```
### 阶段 2通读、匹配与裁决LLM判断
提取和匹配在单次处理中统一完成。规则文件足够小总计约800行可以将全文提供给LLM——无需grep预过滤。
#### 分批处理
根据技能描述,将技能分组为**主题集群**。每个集群在一个子智能体中进行分析,并提供完整的规则文本。
#### 跨批次合并
所有批次完成后,合并各批次的候选规则:
* 对具有相同或重叠原则的候选规则进行去重
* 使用**所有**批次合并的证据重新检查"2+技能"要求——在每个批次中只在一个技能里发现但总计在2+技能中出现的原则是有效的
#### 子智能体提示
使用以下提示启动通用智能体:
````
You are an analyst who cross-reads skills to extract principles that should be promoted to rules.
## Input
- Skills: {full text of skills in this batch}
- Existing rules: {full text of all rule files}
## Extraction Criteria
Include a candidate ONLY if ALL of these are true:
1. **Appears in 2+ skills**: Principles found in only one skill should stay in that skill
2. **Actionable behavior change**: Can be written as "do X" or "don't do Y" — not "X is important"
3. **Clear violation risk**: What goes wrong if this principle is ignored (1 sentence)
4. **Not already in rules**: Check the full rules text — including concepts expressed in different words
## Matching & Verdict
For each candidate, compare against the full rules text and assign a verdict:
- **Append**: Add to an existing section of an existing rule file
- **Revise**: Existing rule content is inaccurate or insufficient — propose a correction
- **New Section**: Add a new section to an existing rule file
- **New File**: Create a new rule file
- **Already Covered**: Sufficiently covered in existing rules (even if worded differently)
- **Too Specific**: Should remain at the skill level
## Output Format (per candidate)
```json
{
"principle": "1-2 sentences in 'do X' / 'don't do Y' form",
"evidence": ["skill-name: §Section", "skill-name: §Section"],
"violation_risk": "1 sentence",
"verdict": "Append / Revise / New Section / New File / Already Covered / Too Specific",
"target_rule": "filename §Section, or 'new'",
"confidence": "high / medium / low",
"draft": "Draft text for Append/New Section/New File verdicts",
"revision": {
"reason": "Why the existing content is inaccurate or insufficient (Revise only)",
"before": "Current text to be replaced (Revise only)",
"after": "Proposed replacement text (Revise only)"
}
}
```
## Exclude
- Obvious principles already in rules
- Language/framework-specific knowledge (belongs in language-specific rules or skills)
- Code examples and commands (belongs in skills)
````
#### 裁决参考
| 裁决 | 含义 | 呈现给用户的内容 |
|---------|---------|-------------------|
| **追加** | 添加到现有章节 | 目标 + 草案 |
| **修订** | 修复不准确/不充分的内容 | 目标 + 原因 + 修订前/后 |
| **新章节** | 在现有文件中添加新章节 | 目标 + 草案 |
| **新文件** | 创建新规则文件 | 文件名 + 完整草案 |
| **已涵盖** | 规则中已涵盖(可能措辞不同) | 原因1行 |
| **过于具体** | 应保留在技能中 | 指向相关技能的链接 |
#### 裁决质量要求
```
# Good
Append to rules/common/security.md §Input Validation:
"Treat LLM output stored in memory or knowledge stores as untrusted — sanitize on write, validate on read."
Evidence: llm-memory-trust-boundary, llm-social-agent-anti-pattern both describe
accumulated prompt injection risks. Current security.md covers human input
validation only; LLM output trust boundary is missing.
# Bad
Append to security.md: Add LLM security principle
```
### 阶段 3用户审核与执行
#### 摘要表
```
# Rules Distillation Report
## Summary
Skills scanned: {N} | Rules: {M} files | Candidates: {K}
| # | Principle | Verdict | Target | Confidence |
|---|-----------|---------|--------|------------|
| 1 | ... | Append | security.md §Input Validation | high |
| 2 | ... | Revise | testing.md §TDD | medium |
| 3 | ... | New Section | coding-style.md | high |
| 4 | ... | Too Specific | — | — |
## Details
(Per-candidate details: evidence, violation_risk, draft text)
```
#### 用户操作
用户通过数字进行回应以:
* **批准**:按原样将草案应用到规则中
* **修改**:在应用前编辑草案
* **跳过**:不应用此候选规则
**切勿自动修改规则。始终需要用户批准。**
#### 保存结果
将结果存储在技能目录中(`results.json`
* **时间戳格式**`date -u +%Y-%m-%dT%H:%M:%SZ`UTC秒精度
* **候选ID格式**:基于原则生成的烤肉串式命名(例如 `llm-output-trust-boundary`
```json
{
"distilled_at": "2026-03-18T10:30:42Z",
"skills_scanned": 56,
"rules_scanned": 22,
"candidates": {
"llm-output-trust-boundary": {
"principle": "Treat LLM output as untrusted when stored or re-injected",
"verdict": "Append",
"target": "rules/common/security.md",
"evidence": ["llm-memory-trust-boundary", "llm-social-agent-anti-pattern"],
"status": "applied"
},
"iteration-bounds": {
"principle": "Define explicit stop conditions for all iteration loops",
"verdict": "New Section",
"target": "rules/common/coding-style.md",
"evidence": ["iterative-retrieval", "continuous-agent-loop", "agent-harness-construction"],
"status": "skipped"
}
}
}
```
## 示例
### 端到端运行
```
$ /rules-distill
Rules Distillation — Phase 1: Inventory
────────────────────────────────────────
Skills: 56 files scanned
Rules: 22 files (75 headings indexed)
Proceeding to cross-read analysis...
[Subagent analysis: Batch 1 (agent/meta skills) ...]
[Subagent analysis: Batch 2 (coding/pattern skills) ...]
[Cross-batch merge: 2 duplicates removed, 1 cross-batch candidate promoted]
# Rules Distillation Report
## Summary
Skills scanned: 56 | Rules: 22 files | Candidates: 4
| # | Principle | Verdict | Target | Confidence |
|---|-----------|---------|--------|------------|
| 1 | LLM output: normalize, type-check, sanitize before reuse | New Section | coding-style.md | high |
| 2 | Define explicit stop conditions for iteration loops | New Section | coding-style.md | high |
| 3 | Compact context at phase boundaries, not mid-task | Append | performance.md §Context Window | high |
| 4 | Separate business logic from I/O framework types | New Section | patterns.md | high |
## Details
### 1. LLM Output Validation
Verdict: New Section in coding-style.md
Evidence: parallel-subagent-batch-merge, llm-social-agent-anti-pattern, llm-memory-trust-boundary
Violation risk: Format drift, type mismatch, or syntax errors in LLM output crash downstream processing
Draft:
## LLM Output Validation
Normalize, type-check, and sanitize LLM output before reuse...
See skill: parallel-subagent-batch-merge, llm-memory-trust-boundary
[... details for candidates 2-4 ...]
Approve, modify, or skip each candidate by number:
> User: Approve 1, 3. Skip 2, 4.
✓ Applied: coding-style.md §LLM Output Validation
✓ Applied: performance.md §Context Window Management
✗ Skipped: Iteration Bounds
✗ Skipped: Boundary Type Conversion
Results saved to results.json
```
## 设计原则
* **是什么,而非如何做**:仅提取原则(规则范畴)。代码示例和命令保留在技能中。
* **链接回源**:草案文本应包含 `See skill: [name]` 引用,以便读者能找到详细的"如何做"。
* **确定性收集LLM判断**脚本保证详尽性LLM保证上下文理解。
* **反抽象保障**三层过滤器2+技能证据、可操作行为测试、违规风险)防止过于抽象的原则进入规则。

View File

@@ -0,0 +1,499 @@
---
name: rust-patterns
description: 地道的Rust模式、所有权、错误处理、特质、并发以及构建安全、高性能应用程序的最佳实践。
origin: ECC
---
# Rust 开发模式
构建安全、高性能且可维护应用程序的惯用 Rust 模式和最佳实践。
## 何时使用
* 编写新的 Rust 代码时
* 评审 Rust 代码时
* 重构现有 Rust 代码时
* 设计 crate 结构和模块布局时
## 工作原理
此技能在六个关键领域强制执行惯用的 Rust 约定:所有权和借用,用于在编译时防止数据竞争;`Result`/`?` 错误传播,库使用 `thiserror` 而应用程序使用 `anyhow`;枚举和穷尽模式匹配,使非法状态无法表示;用于零成本抽象的 trait 和泛型;通过 `Arc<Mutex<T>>`、通道和 async/await 实现的安全并发;以及按领域组织的最小化 `pub` 接口。
## 核心原则
### 1. 所有权和借用
Rust 的所有权系统在编译时防止数据竞争和内存错误。
```rust
// Good: Pass references when you don't need ownership
fn process(data: &[u8]) -> usize {
data.len()
}
// Good: Take ownership only when you need to store or consume
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// Bad: Cloning unnecessarily to avoid borrow checker
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // Wasteful — just borrow
cloned.len()
}
```
### 使用 `Cow` 实现灵活的所有权
```rust
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // Zero-cost when no mutation needed
}
}
```
## 错误处理
### 使用 `Result` 和 `?` —— 切勿在生产环境中使用 `unwrap()`
```rust
// Good: Propagate errors with context
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("failed to parse config from {path}"))?;
Ok(config)
}
// Bad: Panics on error
fn load_config_bad(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // Panics!
toml::from_str(&content).unwrap()
}
```
### 库错误使用 `thiserror`,应用程序错误使用 `anyhow`
```rust
// Library code: structured, typed errors
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("record not found: {id}")]
NotFound { id: String },
#[error("connection failed")]
Connection(#[from] std::io::Error),
#[error("invalid data: {0}")]
InvalidData(String),
}
// Application code: flexible error handling
use anyhow::{bail, Result};
fn run() -> Result<()> {
let config = load_config("app.toml")?;
if config.workers == 0 {
bail!("worker count must be > 0");
}
Ok(())
}
```
### 优先使用 `Option` 组合子而非嵌套匹配
```rust
// Good: Combinator chain
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// Bad: Deeply nested matching
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
match users.iter().find(|u| u.id == id) {
Some(user) => match &user.email {
email => Some(email.clone()),
},
None => None,
}
}
```
## 枚举和模式匹配
### 将状态建模为枚举
```rust
// Good: Impossible states are unrepresentable
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
```
### 穷尽匹配 —— 业务逻辑中不使用通配符
```rust
// Good: Handle every variant explicitly
match command {
Command::Start => start_service(),
Command::Stop => stop_service(),
Command::Restart => restart_service(),
// Adding a new variant forces handling here
}
// Bad: Wildcard hides new variants
match command {
Command::Start => start_service(),
_ => {} // Silently ignores Stop, Restart, and future variants
}
```
## Trait 和泛型
### 接受泛型,返回具体类型
```rust
// Good: Generic input, concrete output
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}
// Good: Trait bounds for multiple constraints
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("processed: {item}")
}
```
### 使用 Trait 对象进行动态分发
```rust
// Use when you need heterogeneous collections or plugin systems
trait Handler: Send + Sync {
fn handle(&self, request: &Request) -> Response;
}
struct Router {
handlers: Vec<Box<dyn Handler>>,
}
// Use generics when you need performance (monomorphization)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
```
### 使用 Newtype 模式确保类型安全
```rust
// Good: Distinct types prevent mixing up arguments
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// Can't accidentally swap user and order IDs
todo!()
}
// Bad: Easy to swap arguments
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
```
## 结构体和数据建模
### 使用构建器模式进行复杂构造
```rust
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
}
}
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
impl ServerConfigBuilder {
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
fn build(self) -> ServerConfig {
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
}
}
// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()
```
## 迭代器和闭包
### 优先使用迭代器链而非手动循环
```rust
// Good: Declarative, lazy, composable
let active_emails: Vec<String> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.clone())
.collect();
// Bad: Imperative accumulation
let mut active_emails = Vec::new();
for user in &users {
if user.is_active {
active_emails.push(user.email.clone());
}
}
```
### 使用带有类型注解的 `collect()`
```rust
// Collect into different types
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();
// Collect Results — short-circuits on first error
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
```
## 并发
### 使用 `Arc<Mutex<T>>` 处理共享可变状态
```rust
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut num = counter.lock().expect("mutex poisoned");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("worker thread panicked");
}
```
### 使用通道进行消息传递
```rust
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressure
for i in 0..5 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(format!("message {i}")).expect("receiver disconnected");
});
}
drop(tx); // Close sender so rx iterator terminates
for msg in rx {
println!("{msg}");
}
```
### 使用 Tokio 进行异步编程
```rust
use tokio::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = tokio::time::timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context("request timed out")?
.context("request failed")?;
response.text().await.context("failed to read body")
}
// Spawn concurrent tasks
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
let handles: Vec<_> = urls.into_iter()
.map(|url| tokio::spawn(async move {
fetch_with_timeout(&url).await
}))
.collect();
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}")));
}
results
}
```
## 不安全代码
### 何时可以使用 Unsafe
```rust
// Acceptable: FFI boundary with documented invariants (Rust 2024+)
/// # Safety
/// `ptr` must be a valid, aligned pointer to an initialized `Widget`.
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
// SAFETY: caller guarantees ptr is valid and aligned
unsafe { &*ptr }
}
// Acceptable: Performance-critical path with proof of correctness
// SAFETY: index is always < len due to the loop bound
unsafe { slice.get_unchecked(index) }
```
### 何时不可以使用 Unsafe
```rust
// Bad: Using unsafe to bypass borrow checker
// Bad: Using unsafe for convenience
// Bad: Using unsafe without a Safety comment
// Bad: Transmuting between unrelated types
```
## 模块系统和 Crate 结构
### 按领域组织,而非按类型
```text
my_app/
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── auth/ # Domain module
│ │ ├── mod.rs
│ │ ├── token.rs
│ │ └── middleware.rs
│ ├── orders/ # Domain module
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ └── service.rs
│ └── db/ # Infrastructure
│ ├── mod.rs
│ └── pool.rs
├── tests/ # Integration tests
├── benches/ # Benchmarks
└── Cargo.toml
```
### 可见性 —— 最小化暴露
```rust
// Good: pub(crate) for internal sharing
pub(crate) fn validate_input(input: &str) -> bool {
!input.is_empty()
}
// Good: Re-export public API from lib.rs
pub mod auth;
pub use auth::AuthMiddleware;
// Bad: Making everything pub
pub fn internal_helper() {} // Should be pub(crate) or private
```
## 工具集成
### 基本命令
```bash
# Build and check
cargo build
cargo check # Fast type checking without codegen
cargo clippy # Lints and suggestions
cargo fmt # Format code
# Testing
cargo test
cargo test -- --nocapture # Show println output
cargo test --lib # Unit tests only
cargo test --test integration # Integration tests only
# Dependencies
cargo audit # Security audit
cargo tree # Dependency tree
cargo update # Update dependencies
# Performance
cargo bench # Run benchmarks
```
## 快速参考Rust 惯用法
| 惯用法 | 描述 |
|-------|-------------|
| 借用,而非克隆 | 传递 `&T`,除非需要所有权,否则不要克隆 |
| 使非法状态无法表示 | 使用枚举仅对有效状态进行建模 |
| `?` 优于 `unwrap()` | 传播错误,切勿在库/生产代码中恐慌 |
| 解析,而非验证 | 在边界处将非结构化数据转换为类型化结构体 |
| Newtype 用于类型安全 | 将基本类型包装在 newtype 中以防止参数错位 |
| 优先使用迭代器而非循环 | 声明式链更清晰且通常更快 |
| 对 Result 使用 `#[must_use]` | 确保调用者处理返回值 |
| 使用 `Cow` 实现灵活的所有权 | 当借用足够时避免分配 |
| 穷尽匹配 | 业务关键枚举不使用通配符 `_` |
| 最小化 `pub` 接口 | 内部 API 使用 `pub(crate)` |
## 应避免的反模式
```rust
// Bad: .unwrap() in production code
let value = map.get("key").unwrap();
// Bad: .clone() to satisfy borrow checker without understanding why
let data = expensive_data.clone();
process(&original, &data);
// Bad: Using String when &str suffices
fn greet(name: String) { /* should be &str */ }
// Bad: Box<dyn Error> in libraries (use thiserror instead)
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
// Bad: Ignoring must_use warnings
let _ = validate(input); // Silently discarding a Result
// Bad: Blocking in async context
async fn bad_async() {
std::thread::sleep(Duration::from_secs(1)); // Blocks the executor!
// Use: tokio::time::sleep(Duration::from_secs(1)).await;
}
```
**请记住**:如果它能编译,那它很可能是正确的 —— 但前提是你要避免 `unwrap()`,最小化 `unsafe`,并让类型系统为你工作。

View File

@@ -0,0 +1,502 @@
---
name: rust-testing
description: Rust测试模式包括单元测试、集成测试、异步测试、基于属性的测试、模拟和覆盖率。遵循TDD方法学。
origin: ECC
---
# Rust 测试模式
遵循 TDD 方法论编写可靠、可维护测试的全面 Rust 测试模式。
## 何时使用
* 编写新的 Rust 函数、方法或特征
* 为现有代码添加测试覆盖率
* 为性能关键代码创建基准测试
* 为输入验证实现基于属性的测试
* 在 Rust 项目中遵循 TDD 工作流
## 工作原理
1. **识别目标代码** — 找到要测试的函数、特征或模块
2. **编写测试** — 在 `#[cfg(test)]` 模块中使用 `#[test]`,使用 rstest 进行参数化测试,或使用 proptest 进行基于属性的测试
3. **模拟依赖项** — 使用 mockall 来隔离被测单元
4. **运行测试 (RED)** — 验证测试是否按预期失败
5. **实现 (GREEN)** — 编写最少代码以通过测试
6. **重构** — 改进代码同时保持测试通过
7. **检查覆盖率** — 使用 cargo-llvm-cov目标 80% 以上
## Rust 的 TDD 工作流
### RED-GREEN-REFACTOR 循环
```
RED → Write a failing test first
GREEN → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT → Continue with next requirement
```
### Rust 中的分步 TDD
```rust
// RED: Write test first, use todo!() as placeholder
pub fn add(a: i32, b: i32) -> i32 { todo!() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() { assert_eq!(add(2, 3), 5); }
}
// cargo test → panics at 'not yet implemented'
```
```rust
// GREEN: Replace todo!() with minimal implementation
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → PASS, then REFACTOR while keeping tests green
```
## 单元测试
### 模块级测试组织
```rust
// src/user.rs
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
let email = email.into();
if !email.contains('@') {
return Err(format!("invalid email: {email}"));
}
Ok(Self { name: name.into(), email })
}
pub fn display_name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.display_name(), "Alice");
assert_eq!(user.email, "alice@example.com");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid email"));
}
}
```
### 断言宏
```rust
assert_eq!(2 + 2, 4); // Equality
assert_ne!(2 + 2, 5); // Inequality
assert!(vec![1, 2, 3].contains(&2)); // Boolean
assert_eq!(value, 42, "expected 42 but got {value}"); // Custom message
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Float comparison
```
## 错误与 Panic 测试
### 测试 `Result` 返回值
```rust
#[test]
fn parse_returns_error_for_invalid_input() {
let result = parse_config("}{invalid");
assert!(result.is_err());
// Assert specific error variant
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::ParseError(_)));
}
#[test]
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
let config = parse_config(r#"{"port": 8080}"#)?;
assert_eq!(config.port, 8080);
Ok(()) // Test fails if any ? returns Err
}
```
### 测试 Panic
```rust
#[test]
#[should_panic]
fn panics_on_empty_input() {
process(&[]);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_specific_message() {
let v: Vec<i32> = vec![];
let _ = v[0];
}
```
## 集成测试
### 文件结构
```text
my_crate/
├── src/
│ └── lib.rs
├── tests/ # Integration tests
│ ├── api_test.rs # Each file is a separate test binary
│ ├── db_test.rs
│ └── common/ # Shared test utilities
│ └── mod.rs
```
### 编写集成测试
```rust
// tests/api_test.rs
use my_crate::{App, Config};
#[test]
fn full_request_lifecycle() {
let config = Config::test_default();
let app = App::new(config);
let response = app.handle_request("/health");
assert_eq!(response.status, 200);
assert_eq!(response.body, "OK");
}
```
## 异步测试
### 使用 Tokio
```rust
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().items.len(), 3);
}
#[tokio::test]
async fn handles_timeout() {
use std::time::Duration;
let result = tokio::time::timeout(
Duration::from_millis(100),
slow_operation(),
).await;
assert!(result.is_err(), "should have timed out");
}
```
## 测试组织模式
### 使用 `rstest` 进行参数化测试
```rust
use rstest::{rstest, fixture};
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
// Fixtures
#[fixture]
fn test_db() -> TestDb {
TestDb::new_in_memory()
}
#[rstest]
fn test_insert(test_db: TestDb) {
test_db.insert("key", "value");
assert_eq!(test_db.get("key"), Some("value".into()));
}
```
### 测试辅助函数
```rust
#[cfg(test)]
mod tests {
use super::*;
/// Creates a test user with sensible defaults.
fn make_user(name: &str) -> User {
User::new(name, &format!("{name}@test.com")).unwrap()
}
#[test]
fn user_display() {
let user = make_user("alice");
assert_eq!(user.display_name(), "alice");
}
}
```
## 使用 `proptest` 进行基于属性的测试
### 基本属性测试
```rust
use proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(input in ".*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded);
}
#[test]
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
let original_len = vec.len();
vec.sort();
assert_eq!(vec.len(), original_len);
}
#[test]
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
vec.sort();
for window in vec.windows(2) {
assert!(window[0] <= window[1]);
}
}
}
```
### 自定义策略
```rust
use proptest::prelude::*;
fn valid_email() -> impl Strategy<Value = String> {
("[a-z]{1,10}", "[a-z]{1,5}")
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
}
proptest! {
#[test]
fn accepts_valid_emails(email in valid_email()) {
assert!(User::new("Test", &email).is_ok());
}
}
```
## 使用 `mockall` 进行模拟
### 基于特征的模拟
```rust
use mockall::{automock, predicate::eq};
#[automock]
trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
fn save(&self, user: &User) -> Result<(), StorageError>;
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn service_returns_none_when_not_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.returning(|_| None);
let service = UserService::new(Box::new(mock));
assert!(service.get_user(99).is_none());
}
```
## 文档测试
### 可执行的文档
````rust
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Parses a config string.
///
/// # Errors
///
/// Returns `Err` if the input is not valid TOML.
///
/// ```no_run
/// use my_crate::parse_config;
///
/// let config = parse_config(r#"port = 8080"#).unwrap();
/// assert_eq!(config.port, 8080);
/// ```
///
/// ```no_run
/// use my_crate::parse_config;
///
/// assert!(parse_config("}{invalid").is_err());
/// ```
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
todo!()
}
````
## 使用 Criterion 进行基准测试
```toml
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "benchmark"
harness = false
```
```rust
// benches/benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
```
## 测试覆盖率
### 运行覆盖率
```bash
# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI)
cargo llvm-cov # Summary
cargo llvm-cov --html # HTML report
cargo llvm-cov --lcov > lcov.info # LCOV format for CI
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
```
### 覆盖率目标
| 代码类型 | 目标 |
|-----------|--------|
| 关键业务逻辑 | 100% |
| 公共 API | 90%+ |
| 通用代码 | 80%+ |
| 生成的 / FFI 绑定 | 排除 |
## 测试命令
```bash
cargo test # Run all tests
cargo test -- --nocapture # Show println output
cargo test test_name # Run tests matching pattern
cargo test --lib # Unit tests only
cargo test --test api_test # Integration tests only
cargo test --doc # Doc tests only
cargo test --no-fail-fast # Don't stop on first failure
cargo test -- --ignored # Run ignored tests
```
## 最佳实践
**应该做:**
* 先写测试 (TDD)
* 使用 `#[cfg(test)]` 模块进行单元测试
* 测试行为,而非实现
* 使用描述性测试名称来解释场景
* 为了更好的错误信息,优先使用 `assert_eq!` 而非 `assert!`
* 在返回 `Result` 的测试中使用 `?` 以获得更清晰的错误输出
* 保持测试独立 — 没有共享的可变状态
**不应该做:**
* 在可以测试 `Result::is_err()` 时使用 `#[should_panic]`
* 模拟所有内容 — 在可行时优先考虑集成测试
* 忽略不稳定的测试 — 修复或隔离它们
* 在测试中使用 `sleep()` — 使用通道、屏障或 `tokio::time::pause()`
* 跳过错误路径测试
## CI 集成
```yaml
# GitHub Actions
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage
run: cargo llvm-cov --fail-under-lines 80
```
**记住**:测试就是文档。它们展示了你的代码应如何使用。清晰编写并保持更新。

View File

@@ -0,0 +1,165 @@
---
name: team-builder
description: 用于组合和派遣并行团队的交互式代理选择器
origin: community
---
# 团队构建器
用于按需浏览和组合智能体团队的交互式菜单。适用于扁平化或按领域子目录组织的智能体集合。
## 使用场景
* 你拥有多个智能体角色markdown 文件),并希望为某项任务选择使用哪些智能体
* 你希望从不同领域(例如,安全 + SEO + 架构)临时组建一个团队
* 你希望在决定前先浏览有哪些可用的智能体
## 前提条件
智能体文件必须是包含角色提示(身份、规则、工作流程、交付物)的 markdown 文件。第一个 `# Heading` 用作智能体名称,第一段用作描述。
支持扁平化和子目录两种布局:
**子目录布局** — 领域从文件夹名称推断:
```
agents/
├── engineering/
│ ├── security-engineer.md
│ └── software-architect.md
├── marketing/
│ └── seo-specialist.md
└── sales/
└── discovery-coach.md
```
**扁平化布局** — 领域从共享的文件名前缀推断。当 2 个或更多文件共享同一前缀时,该前缀被视为一个领域。具有唯一前缀的文件归入 "General" 类别。注意:算法在第一个 `-` 处分割,因此多单词领域(例如 `product-management`)应使用子目录布局:
```
agents/
├── engineering-security-engineer.md
├── engineering-software-architect.md
├── marketing-seo-specialist.md
├── marketing-content-strategist.md
├── sales-discovery-coach.md
└── sales-outbound-strategist.md
```
## 配置
智能体目录按顺序探测,结果会被合并:
1. `./agents/**/*.md` + `./agents/*.md` — 项目本地智能体(两种深度)
2. `~/.claude/agents/**/*.md` + `~/.claude/agents/*.md` — 全局智能体(两种深度)
所有位置的结果会合并,并按智能体名称去重。同名情况下,项目本地智能体优先于全局智能体。如果用户指定了自定义路径,则使用该路径代替。
## 工作原理
### 步骤 1发现可用智能体
使用上述探测顺序在智能体目录中进行全局搜索。排除 README 文件。对于找到的每个文件:
* **子目录布局:** 从父文件夹名称提取领域
* **扁平化布局:** 收集所有文件名前缀(第一个 `-` 之前的文本)。一个前缀只有在出现在 2 个或更多文件名中时才符合领域资格(例如,`engineering-security-engineer.md``engineering-software-architect.md` 都以 `engineering` 开头 → Engineering 领域)。具有唯一前缀的文件(例如 `code-reviewer.md`, `tdd-guide.md`)归入 "General" 类别
* 从第一个 `# Heading` 提取智能体名称。如果未找到标题,则从文件名派生名称(去除 `.md`,用空格替换连字符,并转换为标题大小写)
* 从标题后的第一段提取一行摘要
如果在探测完所有位置后未找到任何智能体文件,则通知用户:"未找到智能体文件。已检查:\[探测的路径列表]。期望:这些目录中的 markdown 文件。" 然后停止。
### 步骤 2呈现领域菜单
```
Available agent domains:
1. Engineering — Software Architect, Security Engineer
2. Marketing — SEO Specialist
3. Sales — Discovery Coach, Outbound Strategist
Pick domains or name specific agents (e.g., "1,3" or "security + seo"):
```
* 跳过智能体数量为零的领域(空目录)
* 显示每个领域的智能体数量
### 步骤 3处理选择
接受灵活的输入:
* 数字:"1,3" 选择 Engineering 和 Sales 中的所有智能体
* 名称:"security + seo" 对发现的智能体进行模糊匹配
* "all from engineering" 选择该领域中的每个智能体
如果选择的智能体超过 5 个,则按字母顺序列出它们,并要求用户缩小范围:"您选择了 N 个智能体(最多 5 个)。请选择保留哪些,或说 'first 5' 以使用按字母顺序排列的前五个。"
确认选择:
```
Selected: Security Engineer + SEO Specialist
What should they work on? (describe the task):
```
### 步骤 4并行启动智能体
1. 读取每个所选智能体的 markdown 文件
2. 如果尚未提供,则提示输入任务描述
3. 使用 Agent 工具并行启动所有智能体:
* `subagent_type: "general-purpose"`
* `prompt: "{agent file content}\n\nTask: {task description}"`
* 每个智能体独立运行 — 不需要智能体间通信
4. 如果某个智能体失败(错误、超时或输出为空),则在内联中注明失败(例如,"Security Engineer: failed — \[原因]")并继续处理成功智能体的结果
### 步骤 5综合结果
收集所有输出并呈现统一的报告:
* 按智能体分组的结果
* 综合部分重点突出:
* 各智能体间的共识
* 建议之间的冲突或矛盾
* 建议的后续步骤
如果只选择了一个智能体,则跳过综合部分,直接呈现输出。
## 规则
* **仅限动态发现。** 切勿硬编码智能体列表。目录中的新文件会自动出现在菜单中。
* **每个团队最多 5 个智能体。** 超过 5 个会产生收益递减和过多的令牌使用。在选择时强制执行。
* **并行分发。** 所有智能体同时运行 — 使用 Agent 工具的并行调用模式。
* **并行 Agent 调用,而非 TeamCreate。** 此技能使用并行 Agent 工具调用来处理独立工作。只有当智能体需要辩论或相互回应时,才需要 TeamCreate一个用于多智能体对话的 Claude Code 工具)。
## 示例
```
User: team builder
Claude:
Available agent domains:
1. Engineering (2) — Software Architect, Security Engineer
2. Marketing (1) — SEO Specialist
3. Sales (4) — Discovery Coach, Outbound Strategist, Proposal Strategist, Sales Engineer
4. Support (1) — Executive Summary
Pick domains or name specific agents:
User: security + seo
Claude:
Selected: Security Engineer + SEO Specialist
What should they work on?
User: Review my Next.js e-commerce site before launch
[Both agents spawn in parallel, each applying their specialty to the codebase]
Claude:
## Security Engineer Findings
- [findings...]
## SEO Specialist Findings
- [findings...]
## Synthesis
Both agents agree on: [...]
Tension: Security recommends CSP that blocks inline styles, SEO needs inline schema markup. Resolution: [...]
Next steps: [...]
```

View File

@@ -92,7 +92,6 @@ def post_thread(oauth, tweets: list[str]) -> list[str]:
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id