mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 13:23:31 +08:00
feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)
ECC community plugin pruning: removed 530+ non-essential files (.cursor/, .opencode/, docs/ja-JP, docs/zh-CN, docs/zh-TW, language-specific skills/agents/rules). Retained 4 agents, 3 commands, 5 skills. Promoted 13 rule files (8 common + 5 typescript) to .claude/rules/ for CC native loading. Extracted reusable patterns to EXTRACTED-PATTERNS.md.
This commit is contained in:
@@ -1,587 +0,0 @@
|
||||
---
|
||||
name: backend-patterns
|
||||
description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes.
|
||||
---
|
||||
|
||||
# 後端開發模式
|
||||
|
||||
用於可擴展伺服器端應用程式的後端架構模式和最佳實務。
|
||||
|
||||
## API 設計模式
|
||||
|
||||
### RESTful API 結構
|
||||
|
||||
```typescript
|
||||
// ✅ 基於資源的 URL
|
||||
GET /api/markets # 列出資源
|
||||
GET /api/markets/:id # 取得單一資源
|
||||
POST /api/markets # 建立資源
|
||||
PUT /api/markets/:id # 替換資源
|
||||
PATCH /api/markets/:id # 更新資源
|
||||
DELETE /api/markets/:id # 刪除資源
|
||||
|
||||
// ✅ 用於過濾、排序、分頁的查詢參數
|
||||
GET /api/markets?status=active&sort=volume&limit=20&offset=0
|
||||
```
|
||||
|
||||
### Repository 模式
|
||||
|
||||
```typescript
|
||||
// 抽象資料存取邏輯
|
||||
interface MarketRepository {
|
||||
findAll(filters?: MarketFilters): Promise<Market[]>
|
||||
findById(id: string): Promise<Market | null>
|
||||
create(data: CreateMarketDto): Promise<Market>
|
||||
update(id: string, data: UpdateMarketDto): Promise<Market>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
|
||||
class SupabaseMarketRepository implements MarketRepository {
|
||||
async findAll(filters?: MarketFilters): Promise<Market[]> {
|
||||
let query = supabase.from('markets').select('*')
|
||||
|
||||
if (filters?.status) {
|
||||
query = query.eq('status', filters.status)
|
||||
}
|
||||
|
||||
if (filters?.limit) {
|
||||
query = query.limit(filters.limit)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) throw new Error(error.message)
|
||||
return data
|
||||
}
|
||||
|
||||
// 其他方法...
|
||||
}
|
||||
```
|
||||
|
||||
### Service 層模式
|
||||
|
||||
```typescript
|
||||
// 業務邏輯與資料存取分離
|
||||
class MarketService {
|
||||
constructor(private marketRepo: MarketRepository) {}
|
||||
|
||||
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
|
||||
// 業務邏輯
|
||||
const embedding = await generateEmbedding(query)
|
||||
const results = await this.vectorSearch(embedding, limit)
|
||||
|
||||
// 取得完整資料
|
||||
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
|
||||
|
||||
// 依相似度排序
|
||||
return markets.sort((a, b) => {
|
||||
const scoreA = results.find(r => r.id === a.id)?.score || 0
|
||||
const scoreB = results.find(r => r.id === b.id)?.score || 0
|
||||
return scoreA - scoreB
|
||||
})
|
||||
}
|
||||
|
||||
private async vectorSearch(embedding: number[], limit: number) {
|
||||
// 向量搜尋實作
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware 模式
|
||||
|
||||
```typescript
|
||||
// 請求/回應處理流水線
|
||||
export function withAuth(handler: NextApiHandler): NextApiHandler {
|
||||
return async (req, res) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await verifyToken(token)
|
||||
req.user = user
|
||||
return handler(req, res)
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
export default withAuth(async (req, res) => {
|
||||
// Handler 可存取 req.user
|
||||
})
|
||||
```
|
||||
|
||||
## 資料庫模式
|
||||
|
||||
### 查詢優化
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:只選擇需要的欄位
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status, volume')
|
||||
.eq('status', 'active')
|
||||
.order('volume', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
// ❌ 不良:選擇所有欄位
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
### N+1 查詢問題預防
|
||||
|
||||
```typescript
|
||||
// ❌ 不良:N+1 查詢問題
|
||||
const markets = await getMarkets()
|
||||
for (const market of markets) {
|
||||
market.creator = await getUser(market.creator_id) // N 次查詢
|
||||
}
|
||||
|
||||
// ✅ 良好:批次取得
|
||||
const markets = await getMarkets()
|
||||
const creatorIds = markets.map(m => m.creator_id)
|
||||
const creators = await getUsers(creatorIds) // 1 次查詢
|
||||
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
||||
|
||||
markets.forEach(market => {
|
||||
market.creator = creatorMap.get(market.creator_id)
|
||||
})
|
||||
```
|
||||
|
||||
### Transaction 模式
|
||||
|
||||
```typescript
|
||||
async function createMarketWithPosition(
|
||||
marketData: CreateMarketDto,
|
||||
positionData: CreatePositionDto
|
||||
) {
|
||||
// 使用 Supabase transaction
|
||||
const { data, error } = await supabase.rpc('create_market_with_position', {
|
||||
market_data: marketData,
|
||||
position_data: positionData
|
||||
})
|
||||
|
||||
if (error) throw new Error('Transaction failed')
|
||||
return data
|
||||
}
|
||||
|
||||
// Supabase 中的 SQL 函式
|
||||
CREATE OR REPLACE FUNCTION create_market_with_position(
|
||||
market_data jsonb,
|
||||
position_data jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- 自動開始 transaction
|
||||
INSERT INTO markets VALUES (market_data);
|
||||
INSERT INTO positions VALUES (position_data);
|
||||
RETURN jsonb_build_object('success', true);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- 自動 rollback
|
||||
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## 快取策略
|
||||
|
||||
### Redis 快取層
|
||||
|
||||
```typescript
|
||||
class CachedMarketRepository implements MarketRepository {
|
||||
constructor(
|
||||
private baseRepo: MarketRepository,
|
||||
private redis: RedisClient
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<Market | null> {
|
||||
// 先檢查快取
|
||||
const cached = await this.redis.get(`market:${id}`)
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
}
|
||||
|
||||
// 快取未命中 - 從資料庫取得
|
||||
const market = await this.baseRepo.findById(id)
|
||||
|
||||
if (market) {
|
||||
// 快取 5 分鐘
|
||||
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
|
||||
}
|
||||
|
||||
return market
|
||||
}
|
||||
|
||||
async invalidateCache(id: string): Promise<void> {
|
||||
await this.redis.del(`market:${id}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache-Aside 模式
|
||||
|
||||
```typescript
|
||||
async function getMarketWithCache(id: string): Promise<Market> {
|
||||
const cacheKey = `market:${id}`
|
||||
|
||||
// 嘗試快取
|
||||
const cached = await redis.get(cacheKey)
|
||||
if (cached) return JSON.parse(cached)
|
||||
|
||||
// 快取未命中 - 從資料庫取得
|
||||
const market = await db.markets.findUnique({ where: { id } })
|
||||
|
||||
if (!market) throw new Error('Market not found')
|
||||
|
||||
// 更新快取
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(market))
|
||||
|
||||
return market
|
||||
}
|
||||
```
|
||||
|
||||
## 錯誤處理模式
|
||||
|
||||
### 集中式錯誤處理器
|
||||
|
||||
```typescript
|
||||
class ApiError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message)
|
||||
Object.setPrototypeOf(this, ApiError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(error: unknown, req: Request): Response {
|
||||
if (error instanceof ApiError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: error.statusCode })
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 記錄非預期錯誤
|
||||
console.error('Unexpected error:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const data = await fetchData()
|
||||
return NextResponse.json({ success: true, data })
|
||||
} catch (error) {
|
||||
return errorHandler(error, request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 指數退避重試
|
||||
|
||||
```typescript
|
||||
async function fetchWithRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
let lastError: Error
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
// 指數退避:1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const data = await fetchWithRetry(() => fetchFromAPI())
|
||||
```
|
||||
|
||||
## 認證與授權
|
||||
|
||||
### JWT Token 驗證
|
||||
|
||||
```typescript
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string
|
||||
email: string
|
||||
role: 'admin' | 'user'
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): JWTPayload {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
|
||||
return payload
|
||||
} catch (error) {
|
||||
throw new ApiError(401, 'Invalid token')
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAuth(request: Request) {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
throw new ApiError(401, 'Missing authorization token')
|
||||
}
|
||||
|
||||
return verifyToken(token)
|
||||
}
|
||||
|
||||
// 在 API 路由中使用
|
||||
export async function GET(request: Request) {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
const data = await getDataForUser(user.userId)
|
||||
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
```
|
||||
|
||||
### 基於角色的存取控制
|
||||
|
||||
```typescript
|
||||
type Permission = 'read' | 'write' | 'delete' | 'admin'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
role: 'admin' | 'moderator' | 'user'
|
||||
}
|
||||
|
||||
const rolePermissions: Record<User['role'], Permission[]> = {
|
||||
admin: ['read', 'write', 'delete', 'admin'],
|
||||
moderator: ['read', 'write', 'delete'],
|
||||
user: ['read', 'write']
|
||||
}
|
||||
|
||||
export function hasPermission(user: User, permission: Permission): boolean {
|
||||
return rolePermissions[user.role].includes(permission)
|
||||
}
|
||||
|
||||
export function requirePermission(permission: Permission) {
|
||||
return (handler: (request: Request, user: User) => Promise<Response>) => {
|
||||
return async (request: Request) => {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw new ApiError(403, 'Insufficient permissions')
|
||||
}
|
||||
|
||||
return handler(request, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式 - HOF 包裝 handler
|
||||
export const DELETE = requirePermission('delete')(
|
||||
async (request: Request, user: User) => {
|
||||
// Handler 接收已驗證且具有已驗證權限的使用者
|
||||
return new Response('Deleted', { status: 200 })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 速率限制
|
||||
|
||||
### 簡單的記憶體速率限制器
|
||||
|
||||
```typescript
|
||||
class RateLimiter {
|
||||
private requests = new Map<string, number[]>()
|
||||
|
||||
async checkLimit(
|
||||
identifier: string,
|
||||
maxRequests: number,
|
||||
windowMs: number
|
||||
): Promise<boolean> {
|
||||
const now = Date.now()
|
||||
const requests = this.requests.get(identifier) || []
|
||||
|
||||
// 移除視窗外的舊請求
|
||||
const recentRequests = requests.filter(time => now - time < windowMs)
|
||||
|
||||
if (recentRequests.length >= maxRequests) {
|
||||
return false // 超過速率限制
|
||||
}
|
||||
|
||||
// 新增當前請求
|
||||
recentRequests.push(now)
|
||||
this.requests.set(identifier, recentRequests)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const limiter = new RateLimiter()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 請求/分鐘
|
||||
|
||||
if (!allowed) {
|
||||
return NextResponse.json({
|
||||
error: 'Rate limit exceeded'
|
||||
}, { status: 429 })
|
||||
}
|
||||
|
||||
// 繼續處理請求
|
||||
}
|
||||
```
|
||||
|
||||
## 背景任務與佇列
|
||||
|
||||
### 簡單佇列模式
|
||||
|
||||
```typescript
|
||||
class JobQueue<T> {
|
||||
private queue: T[] = []
|
||||
private processing = false
|
||||
|
||||
async add(job: T): Promise<void> {
|
||||
this.queue.push(job)
|
||||
|
||||
if (!this.processing) {
|
||||
this.process()
|
||||
}
|
||||
}
|
||||
|
||||
private async process(): Promise<void> {
|
||||
this.processing = true
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const job = this.queue.shift()!
|
||||
|
||||
try {
|
||||
await this.execute(job)
|
||||
} catch (error) {
|
||||
console.error('Job failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
private async execute(job: T): Promise<void> {
|
||||
// 任務執行邏輯
|
||||
}
|
||||
}
|
||||
|
||||
// 用於索引市場的使用範例
|
||||
interface IndexJob {
|
||||
marketId: string
|
||||
}
|
||||
|
||||
const indexQueue = new JobQueue<IndexJob>()
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { marketId } = await request.json()
|
||||
|
||||
// 加入佇列而非阻塞
|
||||
await indexQueue.add({ marketId })
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Job queued' })
|
||||
}
|
||||
```
|
||||
|
||||
## 日誌與監控
|
||||
|
||||
### 結構化日誌
|
||||
|
||||
```typescript
|
||||
interface LogContext {
|
||||
userId?: string
|
||||
requestId?: string
|
||||
method?: string
|
||||
path?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
class Logger {
|
||||
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...context
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(entry))
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext) {
|
||||
this.log('info', message, context)
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext) {
|
||||
this.log('warn', message, context)
|
||||
}
|
||||
|
||||
error(message: string, error: Error, context?: LogContext) {
|
||||
this.log('error', message, {
|
||||
...context,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger()
|
||||
|
||||
// 使用方式
|
||||
export async function GET(request: Request) {
|
||||
const requestId = crypto.randomUUID()
|
||||
|
||||
logger.info('Fetching markets', {
|
||||
requestId,
|
||||
method: 'GET',
|
||||
path: '/api/markets'
|
||||
})
|
||||
|
||||
try {
|
||||
const markets = await fetchMarkets()
|
||||
return NextResponse.json({ success: true, data: markets })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch markets', error as Error, { requestId })
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**記住**:後端模式能實現可擴展、可維護的伺服器端應用程式。選擇符合你複雜度等級的模式。
|
||||
@@ -1,429 +0,0 @@
|
||||
---
|
||||
name: clickhouse-io
|
||||
description: ClickHouse database patterns, query optimization, analytics, and data engineering best practices for high-performance analytical workloads.
|
||||
---
|
||||
|
||||
# ClickHouse 分析模式
|
||||
|
||||
用於高效能分析和資料工程的 ClickHouse 特定模式。
|
||||
|
||||
## 概述
|
||||
|
||||
ClickHouse 是一個列式資料庫管理系統(DBMS),用於線上分析處理(OLAP)。它針對大型資料集的快速分析查詢進行了優化。
|
||||
|
||||
**關鍵特性:**
|
||||
- 列式儲存
|
||||
- 資料壓縮
|
||||
- 平行查詢執行
|
||||
- 分散式查詢
|
||||
- 即時分析
|
||||
|
||||
## 表格設計模式
|
||||
|
||||
### MergeTree 引擎(最常見)
|
||||
|
||||
```sql
|
||||
CREATE TABLE markets_analytics (
|
||||
date Date,
|
||||
market_id String,
|
||||
market_name String,
|
||||
volume UInt64,
|
||||
trades UInt32,
|
||||
unique_traders UInt32,
|
||||
avg_trade_size Float64,
|
||||
created_at DateTime
|
||||
) ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (date, market_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
```
|
||||
|
||||
### ReplacingMergeTree(去重)
|
||||
|
||||
```sql
|
||||
-- 用於可能有重複的資料(例如來自多個來源)
|
||||
CREATE TABLE user_events (
|
||||
event_id String,
|
||||
user_id String,
|
||||
event_type String,
|
||||
timestamp DateTime,
|
||||
properties String
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (user_id, event_id, timestamp)
|
||||
PRIMARY KEY (user_id, event_id);
|
||||
```
|
||||
|
||||
### AggregatingMergeTree(預聚合)
|
||||
|
||||
```sql
|
||||
-- 用於維護聚合指標
|
||||
CREATE TABLE market_stats_hourly (
|
||||
hour DateTime,
|
||||
market_id String,
|
||||
total_volume AggregateFunction(sum, UInt64),
|
||||
total_trades AggregateFunction(count, UInt32),
|
||||
unique_users AggregateFunction(uniq, String)
|
||||
) ENGINE = AggregatingMergeTree()
|
||||
PARTITION BY toYYYYMM(hour)
|
||||
ORDER BY (hour, market_id);
|
||||
|
||||
-- 查詢聚合資料
|
||||
SELECT
|
||||
hour,
|
||||
market_id,
|
||||
sumMerge(total_volume) AS volume,
|
||||
countMerge(total_trades) AS trades,
|
||||
uniqMerge(unique_users) AS users
|
||||
FROM market_stats_hourly
|
||||
WHERE hour >= toStartOfHour(now() - INTERVAL 24 HOUR)
|
||||
GROUP BY hour, market_id
|
||||
ORDER BY hour DESC;
|
||||
```
|
||||
|
||||
## 查詢優化模式
|
||||
|
||||
### 高效過濾
|
||||
|
||||
```sql
|
||||
-- ✅ 良好:先使用索引欄位
|
||||
SELECT *
|
||||
FROM markets_analytics
|
||||
WHERE date >= '2025-01-01'
|
||||
AND market_id = 'market-123'
|
||||
AND volume > 1000
|
||||
ORDER BY date DESC
|
||||
LIMIT 100;
|
||||
|
||||
-- ❌ 不良:先過濾非索引欄位
|
||||
SELECT *
|
||||
FROM markets_analytics
|
||||
WHERE volume > 1000
|
||||
AND market_name LIKE '%election%'
|
||||
AND date >= '2025-01-01';
|
||||
```
|
||||
|
||||
### 聚合
|
||||
|
||||
```sql
|
||||
-- ✅ 良好:使用 ClickHouse 特定聚合函式
|
||||
SELECT
|
||||
toStartOfDay(created_at) AS day,
|
||||
market_id,
|
||||
sum(volume) AS total_volume,
|
||||
count() AS total_trades,
|
||||
uniq(trader_id) AS unique_traders,
|
||||
avg(trade_size) AS avg_size
|
||||
FROM trades
|
||||
WHERE created_at >= today() - INTERVAL 7 DAY
|
||||
GROUP BY day, market_id
|
||||
ORDER BY day DESC, total_volume DESC;
|
||||
|
||||
-- ✅ 使用 quantile 計算百分位數(比 percentile 更高效)
|
||||
SELECT
|
||||
quantile(0.50)(trade_size) AS median,
|
||||
quantile(0.95)(trade_size) AS p95,
|
||||
quantile(0.99)(trade_size) AS p99
|
||||
FROM trades
|
||||
WHERE created_at >= now() - INTERVAL 1 HOUR;
|
||||
```
|
||||
|
||||
### 視窗函式
|
||||
|
||||
```sql
|
||||
-- 計算累計總和
|
||||
SELECT
|
||||
date,
|
||||
market_id,
|
||||
volume,
|
||||
sum(volume) OVER (
|
||||
PARTITION BY market_id
|
||||
ORDER BY date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS cumulative_volume
|
||||
FROM markets_analytics
|
||||
WHERE date >= today() - INTERVAL 30 DAY
|
||||
ORDER BY market_id, date;
|
||||
```
|
||||
|
||||
## 資料插入模式
|
||||
|
||||
### 批量插入(推薦)
|
||||
|
||||
```typescript
|
||||
import { ClickHouse } from 'clickhouse'
|
||||
|
||||
const clickhouse = new ClickHouse({
|
||||
url: process.env.CLICKHOUSE_URL,
|
||||
port: 8123,
|
||||
basicAuth: {
|
||||
username: process.env.CLICKHOUSE_USER,
|
||||
password: process.env.CLICKHOUSE_PASSWORD
|
||||
}
|
||||
})
|
||||
|
||||
// ✅ 批量插入(高效)
|
||||
async function bulkInsertTrades(trades: Trade[]) {
|
||||
const values = trades.map(trade => `(
|
||||
'${trade.id}',
|
||||
'${trade.market_id}',
|
||||
'${trade.user_id}',
|
||||
${trade.amount},
|
||||
'${trade.timestamp.toISOString()}'
|
||||
)`).join(',')
|
||||
|
||||
await clickhouse.query(`
|
||||
INSERT INTO trades (id, market_id, user_id, amount, timestamp)
|
||||
VALUES ${values}
|
||||
`).toPromise()
|
||||
}
|
||||
|
||||
// ❌ 個別插入(慢)
|
||||
async function insertTrade(trade: Trade) {
|
||||
// 不要在迴圈中這樣做!
|
||||
await clickhouse.query(`
|
||||
INSERT INTO trades VALUES ('${trade.id}', ...)
|
||||
`).toPromise()
|
||||
}
|
||||
```
|
||||
|
||||
### 串流插入
|
||||
|
||||
```typescript
|
||||
// 用於持續資料攝取
|
||||
import { createWriteStream } from 'fs'
|
||||
import { pipeline } from 'stream/promises'
|
||||
|
||||
async function streamInserts() {
|
||||
const stream = clickhouse.insert('trades').stream()
|
||||
|
||||
for await (const batch of dataSource) {
|
||||
stream.write(batch)
|
||||
}
|
||||
|
||||
await stream.end()
|
||||
}
|
||||
```
|
||||
|
||||
## 物化視圖
|
||||
|
||||
### 即時聚合
|
||||
|
||||
```sql
|
||||
-- 建立每小時統計的物化視圖
|
||||
CREATE MATERIALIZED VIEW market_stats_hourly_mv
|
||||
TO market_stats_hourly
|
||||
AS SELECT
|
||||
toStartOfHour(timestamp) AS hour,
|
||||
market_id,
|
||||
sumState(amount) AS total_volume,
|
||||
countState() AS total_trades,
|
||||
uniqState(user_id) AS unique_users
|
||||
FROM trades
|
||||
GROUP BY hour, market_id;
|
||||
|
||||
-- 查詢物化視圖
|
||||
SELECT
|
||||
hour,
|
||||
market_id,
|
||||
sumMerge(total_volume) AS volume,
|
||||
countMerge(total_trades) AS trades,
|
||||
uniqMerge(unique_users) AS users
|
||||
FROM market_stats_hourly
|
||||
WHERE hour >= now() - INTERVAL 24 HOUR
|
||||
GROUP BY hour, market_id;
|
||||
```
|
||||
|
||||
## 效能監控
|
||||
|
||||
### 查詢效能
|
||||
|
||||
```sql
|
||||
-- 檢查慢查詢
|
||||
SELECT
|
||||
query_id,
|
||||
user,
|
||||
query,
|
||||
query_duration_ms,
|
||||
read_rows,
|
||||
read_bytes,
|
||||
memory_usage
|
||||
FROM system.query_log
|
||||
WHERE type = 'QueryFinish'
|
||||
AND query_duration_ms > 1000
|
||||
AND event_time >= now() - INTERVAL 1 HOUR
|
||||
ORDER BY query_duration_ms DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### 表格統計
|
||||
|
||||
```sql
|
||||
-- 檢查表格大小
|
||||
SELECT
|
||||
database,
|
||||
table,
|
||||
formatReadableSize(sum(bytes)) AS size,
|
||||
sum(rows) AS rows,
|
||||
max(modification_time) AS latest_modification
|
||||
FROM system.parts
|
||||
WHERE active
|
||||
GROUP BY database, table
|
||||
ORDER BY sum(bytes) DESC;
|
||||
```
|
||||
|
||||
## 常見分析查詢
|
||||
|
||||
### 時間序列分析
|
||||
|
||||
```sql
|
||||
-- 每日活躍使用者
|
||||
SELECT
|
||||
toDate(timestamp) AS date,
|
||||
uniq(user_id) AS daily_active_users
|
||||
FROM events
|
||||
WHERE timestamp >= today() - INTERVAL 30 DAY
|
||||
GROUP BY date
|
||||
ORDER BY date;
|
||||
|
||||
-- 留存分析
|
||||
SELECT
|
||||
signup_date,
|
||||
countIf(days_since_signup = 0) AS day_0,
|
||||
countIf(days_since_signup = 1) AS day_1,
|
||||
countIf(days_since_signup = 7) AS day_7,
|
||||
countIf(days_since_signup = 30) AS day_30
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
min(toDate(timestamp)) AS signup_date,
|
||||
toDate(timestamp) AS activity_date,
|
||||
dateDiff('day', signup_date, activity_date) AS days_since_signup
|
||||
FROM events
|
||||
GROUP BY user_id, activity_date
|
||||
)
|
||||
GROUP BY signup_date
|
||||
ORDER BY signup_date DESC;
|
||||
```
|
||||
|
||||
### 漏斗分析
|
||||
|
||||
```sql
|
||||
-- 轉換漏斗
|
||||
SELECT
|
||||
countIf(step = 'viewed_market') AS viewed,
|
||||
countIf(step = 'clicked_trade') AS clicked,
|
||||
countIf(step = 'completed_trade') AS completed,
|
||||
round(clicked / viewed * 100, 2) AS view_to_click_rate,
|
||||
round(completed / clicked * 100, 2) AS click_to_completion_rate
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
session_id,
|
||||
event_type AS step
|
||||
FROM events
|
||||
WHERE event_date = today()
|
||||
)
|
||||
GROUP BY session_id;
|
||||
```
|
||||
|
||||
### 世代分析
|
||||
|
||||
```sql
|
||||
-- 按註冊月份的使用者世代
|
||||
SELECT
|
||||
toStartOfMonth(signup_date) AS cohort,
|
||||
toStartOfMonth(activity_date) AS month,
|
||||
dateDiff('month', cohort, month) AS months_since_signup,
|
||||
count(DISTINCT user_id) AS active_users
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
min(toDate(timestamp)) OVER (PARTITION BY user_id) AS signup_date,
|
||||
toDate(timestamp) AS activity_date
|
||||
FROM events
|
||||
)
|
||||
GROUP BY cohort, month, months_since_signup
|
||||
ORDER BY cohort, months_since_signup;
|
||||
```
|
||||
|
||||
## 資料管線模式
|
||||
|
||||
### ETL 模式
|
||||
|
||||
```typescript
|
||||
// 提取、轉換、載入
|
||||
async function etlPipeline() {
|
||||
// 1. 從來源提取
|
||||
const rawData = await extractFromPostgres()
|
||||
|
||||
// 2. 轉換
|
||||
const transformed = rawData.map(row => ({
|
||||
date: new Date(row.created_at).toISOString().split('T')[0],
|
||||
market_id: row.market_slug,
|
||||
volume: parseFloat(row.total_volume),
|
||||
trades: parseInt(row.trade_count)
|
||||
}))
|
||||
|
||||
// 3. 載入到 ClickHouse
|
||||
await bulkInsertToClickHouse(transformed)
|
||||
}
|
||||
|
||||
// 定期執行
|
||||
setInterval(etlPipeline, 60 * 60 * 1000) // 每小時
|
||||
```
|
||||
|
||||
### 變更資料捕獲(CDC)
|
||||
|
||||
```typescript
|
||||
// 監聽 PostgreSQL 變更並同步到 ClickHouse
|
||||
import { Client } from 'pg'
|
||||
|
||||
const pgClient = new Client({ connectionString: process.env.DATABASE_URL })
|
||||
|
||||
pgClient.query('LISTEN market_updates')
|
||||
|
||||
pgClient.on('notification', async (msg) => {
|
||||
const update = JSON.parse(msg.payload)
|
||||
|
||||
await clickhouse.insert('market_updates', [
|
||||
{
|
||||
market_id: update.id,
|
||||
event_type: update.operation, // INSERT, UPDATE, DELETE
|
||||
timestamp: new Date(),
|
||||
data: JSON.stringify(update.new_data)
|
||||
}
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
## 最佳實務
|
||||
|
||||
### 1. 分區策略
|
||||
- 按時間分區(通常按月或日)
|
||||
- 避免太多分區(效能影響)
|
||||
- 分區鍵使用 DATE 類型
|
||||
|
||||
### 2. 排序鍵
|
||||
- 最常過濾的欄位放在最前面
|
||||
- 考慮基數(高基數優先)
|
||||
- 排序影響壓縮
|
||||
|
||||
### 3. 資料類型
|
||||
- 使用最小的適當類型(UInt32 vs UInt64)
|
||||
- 重複字串使用 LowCardinality
|
||||
- 分類資料使用 Enum
|
||||
|
||||
### 4. 避免
|
||||
- SELECT *(指定欄位)
|
||||
- FINAL(改為在查詢前合併資料)
|
||||
- 太多 JOINs(為分析反正規化)
|
||||
- 小量頻繁插入(改用批量)
|
||||
|
||||
### 5. 監控
|
||||
- 追蹤查詢效能
|
||||
- 監控磁碟使用
|
||||
- 檢查合併操作
|
||||
- 審查慢查詢日誌
|
||||
|
||||
**記住**:ClickHouse 擅長分析工作負載。為你的查詢模式設計表格,批量插入,並利用物化視圖進行即時聚合。
|
||||
@@ -1,520 +0,0 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: Universal coding standards, best practices, and patterns for TypeScript, JavaScript, React, and Node.js development.
|
||||
---
|
||||
|
||||
# 程式碼標準與最佳實務
|
||||
|
||||
適用於所有專案的通用程式碼標準。
|
||||
|
||||
## 程式碼品質原則
|
||||
|
||||
### 1. 可讀性優先
|
||||
- 程式碼被閱讀的次數遠多於被撰寫的次數
|
||||
- 使用清晰的變數和函式名稱
|
||||
- 優先使用自文件化的程式碼而非註解
|
||||
- 保持一致的格式化
|
||||
|
||||
### 2. KISS(保持簡單)
|
||||
- 使用最簡單的解決方案
|
||||
- 避免過度工程
|
||||
- 不做過早優化
|
||||
- 易於理解 > 聰明的程式碼
|
||||
|
||||
### 3. DRY(不重複自己)
|
||||
- 將共用邏輯提取為函式
|
||||
- 建立可重用的元件
|
||||
- 在模組間共享工具函式
|
||||
- 避免複製貼上程式設計
|
||||
|
||||
### 4. YAGNI(你不會需要它)
|
||||
- 在需要之前不要建置功能
|
||||
- 避免推測性的通用化
|
||||
- 只在需要時增加複雜度
|
||||
- 從簡單開始,需要時再重構
|
||||
|
||||
## TypeScript/JavaScript 標準
|
||||
|
||||
### 變數命名
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:描述性名稱
|
||||
const marketSearchQuery = 'election'
|
||||
const isUserAuthenticated = true
|
||||
const totalRevenue = 1000
|
||||
|
||||
// ❌ 不良:不清楚的名稱
|
||||
const q = 'election'
|
||||
const flag = true
|
||||
const x = 1000
|
||||
```
|
||||
|
||||
### 函式命名
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:動詞-名詞模式
|
||||
async function fetchMarketData(marketId: string) { }
|
||||
function calculateSimilarity(a: number[], b: number[]) { }
|
||||
function isValidEmail(email: string): boolean { }
|
||||
|
||||
// ❌ 不良:不清楚或只有名詞
|
||||
async function market(id: string) { }
|
||||
function similarity(a, b) { }
|
||||
function email(e) { }
|
||||
```
|
||||
|
||||
### 不可變性模式(關鍵)
|
||||
|
||||
```typescript
|
||||
// ✅ 總是使用展開運算符
|
||||
const updatedUser = {
|
||||
...user,
|
||||
name: 'New Name'
|
||||
}
|
||||
|
||||
const updatedArray = [...items, newItem]
|
||||
|
||||
// ❌ 永遠不要直接修改
|
||||
user.name = 'New Name' // 不良
|
||||
items.push(newItem) // 不良
|
||||
```
|
||||
|
||||
### 錯誤處理
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:完整的錯誤處理
|
||||
async function fetchData(url: string) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch failed:', error)
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不良:無錯誤處理
|
||||
async function fetchData(url) {
|
||||
const response = await fetch(url)
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await 最佳實務
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:可能時並行執行
|
||||
const [users, markets, stats] = await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchMarkets(),
|
||||
fetchStats()
|
||||
])
|
||||
|
||||
// ❌ 不良:不必要的順序執行
|
||||
const users = await fetchUsers()
|
||||
const markets = await fetchMarkets()
|
||||
const stats = await fetchStats()
|
||||
```
|
||||
|
||||
### 型別安全
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:正確的型別
|
||||
interface Market {
|
||||
id: string
|
||||
name: string
|
||||
status: 'active' | 'resolved' | 'closed'
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
function getMarket(id: string): Promise<Market> {
|
||||
// 實作
|
||||
}
|
||||
|
||||
// ❌ 不良:使用 'any'
|
||||
function getMarket(id: any): Promise<any> {
|
||||
// 實作
|
||||
}
|
||||
```
|
||||
|
||||
## React 最佳實務
|
||||
|
||||
### 元件結構
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:具有型別的函式元件
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
variant = 'primary'
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`btn btn-${variant}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ❌ 不良:無型別、結構不清楚
|
||||
export function Button(props) {
|
||||
return <button onClick={props.onClick}>{props.children}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### 自訂 Hooks
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:可重用的自訂 hook
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||
```
|
||||
|
||||
### 狀態管理
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:正確的狀態更新
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// 基於先前狀態的函式更新
|
||||
setCount(prev => prev + 1)
|
||||
|
||||
// ❌ 不良:直接引用狀態
|
||||
setCount(count + 1) // 在非同步情境中可能過時
|
||||
```
|
||||
|
||||
### 條件渲染
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:清晰的條件渲染
|
||||
{isLoading && <Spinner />}
|
||||
{error && <ErrorMessage error={error} />}
|
||||
{data && <DataDisplay data={data} />}
|
||||
|
||||
// ❌ 不良:三元地獄
|
||||
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
|
||||
```
|
||||
|
||||
## API 設計標準
|
||||
|
||||
### REST API 慣例
|
||||
|
||||
```
|
||||
GET /api/markets # 列出所有市場
|
||||
GET /api/markets/:id # 取得特定市場
|
||||
POST /api/markets # 建立新市場
|
||||
PUT /api/markets/:id # 更新市場(完整)
|
||||
PATCH /api/markets/:id # 更新市場(部分)
|
||||
DELETE /api/markets/:id # 刪除市場
|
||||
|
||||
# 過濾用查詢參數
|
||||
GET /api/markets?status=active&limit=10&offset=0
|
||||
```
|
||||
|
||||
### 回應格式
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:一致的回應結構
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
meta?: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
// 成功回應
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: markets,
|
||||
meta: { total: 100, page: 1, limit: 10 }
|
||||
})
|
||||
|
||||
// 錯誤回應
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Invalid request'
|
||||
}, { status: 400 })
|
||||
```
|
||||
|
||||
### 輸入驗證
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// ✅ 良好:Schema 驗證
|
||||
const CreateMarketSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().min(1).max(2000),
|
||||
endDate: z.string().datetime(),
|
||||
categories: z.array(z.string()).min(1)
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
try {
|
||||
const validated = CreateMarketSchema.parse(body)
|
||||
// 使用驗證過的資料繼續處理
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 檔案組織
|
||||
|
||||
### 專案結構
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API 路由
|
||||
│ ├── markets/ # 市場頁面
|
||||
│ └── (auth)/ # 認證頁面(路由群組)
|
||||
├── components/ # React 元件
|
||||
│ ├── ui/ # 通用 UI 元件
|
||||
│ ├── forms/ # 表單元件
|
||||
│ └── layouts/ # 版面配置元件
|
||||
├── hooks/ # 自訂 React hooks
|
||||
├── lib/ # 工具和設定
|
||||
│ ├── api/ # API 客戶端
|
||||
│ ├── utils/ # 輔助函式
|
||||
│ └── constants/ # 常數
|
||||
├── types/ # TypeScript 型別
|
||||
└── styles/ # 全域樣式
|
||||
```
|
||||
|
||||
### 檔案命名
|
||||
|
||||
```
|
||||
components/Button.tsx # 元件用 PascalCase
|
||||
hooks/useAuth.ts # hooks 用 camelCase 加 'use' 前綴
|
||||
lib/formatDate.ts # 工具用 camelCase
|
||||
types/market.types.ts # 型別用 camelCase 加 .types 後綴
|
||||
```
|
||||
|
||||
## 註解與文件
|
||||
|
||||
### 何時註解
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:解釋「為什麼」而非「什麼」
|
||||
// 使用指數退避以避免在服務中斷時壓垮 API
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
|
||||
|
||||
// 為了處理大陣列的效能,此處刻意使用突變
|
||||
items.push(newItem)
|
||||
|
||||
// ❌ 不良:陳述顯而易見的事實
|
||||
// 將計數器加 1
|
||||
count++
|
||||
|
||||
// 將名稱設為使用者的名稱
|
||||
name = user.name
|
||||
```
|
||||
|
||||
### 公開 API 的 JSDoc
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 使用語意相似度搜尋市場。
|
||||
*
|
||||
* @param query - 自然語言搜尋查詢
|
||||
* @param limit - 最大結果數量(預設:10)
|
||||
* @returns 按相似度分數排序的市場陣列
|
||||
* @throws {Error} 如果 OpenAI API 失敗或 Redis 不可用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await searchMarkets('election', 5)
|
||||
* console.log(results[0].name) // "Trump vs Biden"
|
||||
* ```
|
||||
*/
|
||||
export async function searchMarkets(
|
||||
query: string,
|
||||
limit: number = 10
|
||||
): Promise<Market[]> {
|
||||
// 實作
|
||||
}
|
||||
```
|
||||
|
||||
## 效能最佳實務
|
||||
|
||||
### 記憶化
|
||||
|
||||
```typescript
|
||||
import { useMemo, useCallback } from 'react'
|
||||
|
||||
// ✅ 良好:記憶化昂貴的計算
|
||||
const sortedMarkets = useMemo(() => {
|
||||
return markets.sort((a, b) => b.volume - a.volume)
|
||||
}, [markets])
|
||||
|
||||
// ✅ 良好:記憶化回呼函式
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### 延遲載入
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// ✅ 良好:延遲載入重型元件
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<HeavyChart />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 資料庫查詢
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:只選擇需要的欄位
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status')
|
||||
.limit(10)
|
||||
|
||||
// ❌ 不良:選擇所有欄位
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
## 測試標準
|
||||
|
||||
### 測試結構(AAA 模式)
|
||||
|
||||
```typescript
|
||||
test('calculates similarity correctly', () => {
|
||||
// Arrange(準備)
|
||||
const vector1 = [1, 0, 0]
|
||||
const vector2 = [0, 1, 0]
|
||||
|
||||
// Act(執行)
|
||||
const similarity = calculateCosineSimilarity(vector1, vector2)
|
||||
|
||||
// Assert(斷言)
|
||||
expect(similarity).toBe(0)
|
||||
})
|
||||
```
|
||||
|
||||
### 測試命名
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:描述性測試名稱
|
||||
test('returns empty array when no markets match query', () => { })
|
||||
test('throws error when OpenAI API key is missing', () => { })
|
||||
test('falls back to substring search when Redis unavailable', () => { })
|
||||
|
||||
// ❌ 不良:模糊的測試名稱
|
||||
test('works', () => { })
|
||||
test('test search', () => { })
|
||||
```
|
||||
|
||||
## 程式碼異味偵測
|
||||
|
||||
注意這些反模式:
|
||||
|
||||
### 1. 過長函式
|
||||
```typescript
|
||||
// ❌ 不良:函式超過 50 行
|
||||
function processMarketData() {
|
||||
// 100 行程式碼
|
||||
}
|
||||
|
||||
// ✅ 良好:拆分為較小的函式
|
||||
function processMarketData() {
|
||||
const validated = validateData()
|
||||
const transformed = transformData(validated)
|
||||
return saveData(transformed)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 過深巢狀
|
||||
```typescript
|
||||
// ❌ 不良:5 層以上巢狀
|
||||
if (user) {
|
||||
if (user.isAdmin) {
|
||||
if (market) {
|
||||
if (market.isActive) {
|
||||
if (hasPermission) {
|
||||
// 做某事
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 良好:提前返回
|
||||
if (!user) return
|
||||
if (!user.isAdmin) return
|
||||
if (!market) return
|
||||
if (!market.isActive) return
|
||||
if (!hasPermission) return
|
||||
|
||||
// 做某事
|
||||
```
|
||||
|
||||
### 3. 魔術數字
|
||||
```typescript
|
||||
// ❌ 不良:無解釋的數字
|
||||
if (retryCount > 3) { }
|
||||
setTimeout(callback, 500)
|
||||
|
||||
// ✅ 良好:命名常數
|
||||
const MAX_RETRIES = 3
|
||||
const DEBOUNCE_DELAY_MS = 500
|
||||
|
||||
if (retryCount > MAX_RETRIES) { }
|
||||
setTimeout(callback, DEBOUNCE_DELAY_MS)
|
||||
```
|
||||
|
||||
**記住**:程式碼品質是不可協商的。清晰、可維護的程式碼能實現快速開發和自信的重構。
|
||||
@@ -1,257 +0,0 @@
|
||||
---
|
||||
name: continuous-learning-v2
|
||||
description: Instinct-based learning system that observes sessions via hooks, creates atomic instincts with confidence scoring, and evolves them into skills/commands/agents.
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# 持續學習 v2 - 基於本能的架構
|
||||
|
||||
進階學習系統,透過原子「本能」(帶信心評分的小型學習行為)將你的 Claude Code 工作階段轉化為可重用知識。
|
||||
|
||||
## v2 的新功能
|
||||
|
||||
| 功能 | v1 | v2 |
|
||||
|------|----|----|
|
||||
| 觀察 | Stop hook(工作階段結束) | PreToolUse/PostToolUse(100% 可靠) |
|
||||
| 分析 | 主要上下文 | 背景 agent(Haiku) |
|
||||
| 粒度 | 完整技能 | 原子「本能」 |
|
||||
| 信心 | 無 | 0.3-0.9 加權 |
|
||||
| 演化 | 直接到技能 | 本能 → 聚類 → 技能/指令/agent |
|
||||
| 分享 | 無 | 匯出/匯入本能 |
|
||||
|
||||
## 本能模型
|
||||
|
||||
本能是一個小型學習行為:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: prefer-functional-style
|
||||
trigger: "when writing new functions"
|
||||
confidence: 0.7
|
||||
domain: "code-style"
|
||||
source: "session-observation"
|
||||
---
|
||||
|
||||
# 偏好函式風格
|
||||
|
||||
## 動作
|
||||
適當時使用函式模式而非類別。
|
||||
|
||||
## 證據
|
||||
- 觀察到 5 次函式模式偏好
|
||||
- 使用者在 2025-01-15 將基於類別的方法修正為函式
|
||||
```
|
||||
|
||||
**屬性:**
|
||||
- **原子性** — 一個觸發器,一個動作
|
||||
- **信心加權** — 0.3 = 試探性,0.9 = 近乎確定
|
||||
- **領域標記** — code-style、testing、git、debugging、workflow 等
|
||||
- **證據支持** — 追蹤建立它的觀察
|
||||
|
||||
## 運作方式
|
||||
|
||||
```
|
||||
工作階段活動
|
||||
│
|
||||
│ Hooks 捕獲提示 + 工具使用(100% 可靠)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ observations.jsonl │
|
||||
│ (提示、工具呼叫、結果) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ Observer agent 讀取(背景、Haiku)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 模式偵測 │
|
||||
│ • 使用者修正 → 本能 │
|
||||
│ • 錯誤解決 → 本能 │
|
||||
│ • 重複工作流程 → 本能 │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ 建立/更新
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ instincts/personal/ │
|
||||
│ • prefer-functional.md (0.7) │
|
||||
│ • always-test-first.md (0.9) │
|
||||
│ • use-zod-validation.md (0.6) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ /evolve 聚類
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ evolved/ │
|
||||
│ • commands/new-feature.md │
|
||||
│ • skills/testing-workflow.md │
|
||||
│ • agents/refactor-specialist.md │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 1. 啟用觀察 Hooks
|
||||
|
||||
新增到你的 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre"
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 初始化目錄結構
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands}}
|
||||
touch ~/.claude/homunculus/observations.jsonl
|
||||
```
|
||||
|
||||
### 3. 執行 Observer Agent(可選)
|
||||
|
||||
觀察者可以在背景執行並分析觀察:
|
||||
|
||||
```bash
|
||||
# 啟動背景觀察者
|
||||
~/.claude/skills/continuous-learning-v2/agents/start-observer.sh
|
||||
```
|
||||
|
||||
## 指令
|
||||
|
||||
| 指令 | 描述 |
|
||||
|------|------|
|
||||
| `/instinct-status` | 顯示所有學習本能及其信心 |
|
||||
| `/evolve` | 將相關本能聚類為技能/指令 |
|
||||
| `/instinct-export` | 匯出本能以分享 |
|
||||
| `/instinct-import <file>` | 從他人匯入本能 |
|
||||
|
||||
## 設定
|
||||
|
||||
編輯 `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"observation": {
|
||||
"enabled": true,
|
||||
"store_path": "~/.claude/homunculus/observations.jsonl",
|
||||
"max_file_size_mb": 10,
|
||||
"archive_after_days": 7
|
||||
},
|
||||
"instincts": {
|
||||
"personal_path": "~/.claude/homunculus/instincts/personal/",
|
||||
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
|
||||
"min_confidence": 0.3,
|
||||
"auto_approve_threshold": 0.7,
|
||||
"confidence_decay_rate": 0.05
|
||||
},
|
||||
"observer": {
|
||||
"enabled": true,
|
||||
"model": "haiku",
|
||||
"run_interval_minutes": 5,
|
||||
"patterns_to_detect": [
|
||||
"user_corrections",
|
||||
"error_resolutions",
|
||||
"repeated_workflows",
|
||||
"tool_preferences"
|
||||
]
|
||||
},
|
||||
"evolution": {
|
||||
"cluster_threshold": 3,
|
||||
"evolved_path": "~/.claude/homunculus/evolved/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 檔案結構
|
||||
|
||||
```
|
||||
~/.claude/homunculus/
|
||||
├── identity.json # 你的個人資料、技術水平
|
||||
├── observations.jsonl # 當前工作階段觀察
|
||||
├── observations.archive/ # 已處理觀察
|
||||
├── instincts/
|
||||
│ ├── personal/ # 自動學習本能
|
||||
│ └── inherited/ # 從他人匯入
|
||||
└── evolved/
|
||||
├── agents/ # 產生的專業 agents
|
||||
├── skills/ # 產生的技能
|
||||
└── commands/ # 產生的指令
|
||||
```
|
||||
|
||||
## 與 Skill Creator 整合
|
||||
|
||||
當你使用 [Skill Creator GitHub App](https://skill-creator.app) 時,它現在產生**兩者**:
|
||||
- 傳統 SKILL.md 檔案(用於向後相容)
|
||||
- 本能集合(用於 v2 學習系統)
|
||||
|
||||
從倉庫分析的本能有 `source: "repo-analysis"` 並包含來源倉庫 URL。
|
||||
|
||||
## 信心評分
|
||||
|
||||
信心隨時間演化:
|
||||
|
||||
| 分數 | 意義 | 行為 |
|
||||
|------|------|------|
|
||||
| 0.3 | 試探性 | 建議但不強制 |
|
||||
| 0.5 | 中等 | 相關時應用 |
|
||||
| 0.7 | 強烈 | 自動批准應用 |
|
||||
| 0.9 | 近乎確定 | 核心行為 |
|
||||
|
||||
**信心增加**當:
|
||||
- 重複觀察到模式
|
||||
- 使用者不修正建議行為
|
||||
- 來自其他來源的類似本能同意
|
||||
|
||||
**信心減少**當:
|
||||
- 使用者明確修正行為
|
||||
- 長期未觀察到模式
|
||||
- 出現矛盾證據
|
||||
|
||||
## 為何 Hooks vs Skills 用於觀察?
|
||||
|
||||
> "v1 依賴技能進行觀察。技能是機率性的——它們根據 Claude 的判斷觸發約 50-80% 的時間。"
|
||||
|
||||
Hooks **100% 的時間**確定性地觸發。這意味著:
|
||||
- 每個工具呼叫都被觀察
|
||||
- 無模式被遺漏
|
||||
- 學習是全面的
|
||||
|
||||
## 向後相容性
|
||||
|
||||
v2 完全相容 v1:
|
||||
- 現有 `~/.claude/skills/learned/` 技能仍可運作
|
||||
- Stop hook 仍執行(但現在也餵入 v2)
|
||||
- 漸進遷移路徑:兩者並行執行
|
||||
|
||||
## 隱私
|
||||
|
||||
- 觀察保持在你的機器**本機**
|
||||
- 只有**本能**(模式)可被匯出
|
||||
- 不會分享實際程式碼或對話內容
|
||||
- 你控制匯出內容
|
||||
|
||||
## 相關
|
||||
|
||||
- [Skill Creator](https://skill-creator.app) - 從倉庫歷史產生本能
|
||||
- [Homunculus](https://github.com/humanplane/homunculus) - v2 架構靈感
|
||||
- [Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 持續學習章節
|
||||
|
||||
---
|
||||
|
||||
*基於本能的學習:一次一個觀察,教導 Claude 你的模式。*
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
name: continuous-learning
|
||||
description: Automatically extract reusable patterns from Claude Code sessions and save them as learned skills for future use.
|
||||
---
|
||||
|
||||
# 持續學習技能
|
||||
|
||||
自動評估 Claude Code 工作階段結束時的內容,提取可重用模式並儲存為學習技能。
|
||||
|
||||
## 運作方式
|
||||
|
||||
此技能作為 **Stop hook** 在每個工作階段結束時執行:
|
||||
|
||||
1. **工作階段評估**:檢查工作階段是否有足夠訊息(預設:10+ 則)
|
||||
2. **模式偵測**:從工作階段識別可提取的模式
|
||||
3. **技能提取**:將有用模式儲存到 `~/.claude/skills/learned/`
|
||||
|
||||
## 設定
|
||||
|
||||
編輯 `config.json` 以自訂:
|
||||
|
||||
```json
|
||||
{
|
||||
"min_session_length": 10,
|
||||
"extraction_threshold": "medium",
|
||||
"auto_approve": false,
|
||||
"learned_skills_path": "~/.claude/skills/learned/",
|
||||
"patterns_to_detect": [
|
||||
"error_resolution",
|
||||
"user_corrections",
|
||||
"workarounds",
|
||||
"debugging_techniques",
|
||||
"project_specific"
|
||||
],
|
||||
"ignore_patterns": [
|
||||
"simple_typos",
|
||||
"one_time_fixes",
|
||||
"external_api_issues"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 模式類型
|
||||
|
||||
| 模式 | 描述 |
|
||||
|------|------|
|
||||
| `error_resolution` | 特定錯誤如何被解決 |
|
||||
| `user_corrections` | 來自使用者修正的模式 |
|
||||
| `workarounds` | 框架/函式庫怪異問題的解決方案 |
|
||||
| `debugging_techniques` | 有效的除錯方法 |
|
||||
| `project_specific` | 專案特定慣例 |
|
||||
|
||||
## Hook 設定
|
||||
|
||||
新增到你的 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 為什麼用 Stop Hook?
|
||||
|
||||
- **輕量**:工作階段結束時只執行一次
|
||||
- **非阻塞**:不會為每則訊息增加延遲
|
||||
- **完整上下文**:可存取完整工作階段記錄
|
||||
|
||||
## 相關
|
||||
|
||||
- [Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 持續學習章節
|
||||
- `/learn` 指令 - 工作階段中手動提取模式
|
||||
|
||||
---
|
||||
|
||||
## 比較筆記(研究:2025 年 1 月)
|
||||
|
||||
### vs Homunculus (github.com/humanplane/homunculus)
|
||||
|
||||
Homunculus v2 採用更複雜的方法:
|
||||
|
||||
| 功能 | 我們的方法 | Homunculus v2 |
|
||||
|------|----------|---------------|
|
||||
| 觀察 | Stop hook(工作階段結束) | PreToolUse/PostToolUse hooks(100% 可靠) |
|
||||
| 分析 | 主要上下文 | 背景 agent(Haiku) |
|
||||
| 粒度 | 完整技能 | 原子「本能」 |
|
||||
| 信心 | 無 | 0.3-0.9 加權 |
|
||||
| 演化 | 直接到技能 | 本能 → 聚類 → 技能/指令/agent |
|
||||
| 分享 | 無 | 匯出/匯入本能 |
|
||||
|
||||
**來自 homunculus 的關鍵見解:**
|
||||
> "v1 依賴技能進行觀察。技能是機率性的——它們觸發約 50-80% 的時間。v2 使用 hooks 進行觀察(100% 可靠),並以本能作為學習行為的原子單位。"
|
||||
|
||||
### 潛在 v2 增強
|
||||
|
||||
1. **基於本能的學習** - 較小的原子行為,帶信心評分
|
||||
2. **背景觀察者** - Haiku agent 並行分析
|
||||
3. **信心衰減** - 如果被矛盾則本能失去信心
|
||||
4. **領域標記** - code-style、testing、git、debugging 等
|
||||
5. **演化路徑** - 將相關本能聚類為技能/指令
|
||||
|
||||
參見:`/Users/affoon/Documents/tasks/12-continuous-learning-v2.md` 完整規格。
|
||||
@@ -1,227 +0,0 @@
|
||||
---
|
||||
name: eval-harness
|
||||
description: Formal evaluation framework for Claude Code sessions implementing eval-driven development (EDD) principles
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Eval Harness 技能
|
||||
|
||||
Claude Code 工作階段的正式評估框架,實作 eval 驅動開發(EDD)原則。
|
||||
|
||||
## 理念
|
||||
|
||||
Eval 驅動開發將 evals 視為「AI 開發的單元測試」:
|
||||
- 在實作前定義預期行為
|
||||
- 開發期間持續執行 evals
|
||||
- 每次變更追蹤回歸
|
||||
- 使用 pass@k 指標進行可靠性測量
|
||||
|
||||
## Eval 類型
|
||||
|
||||
### 能力 Evals
|
||||
測試 Claude 是否能做到以前做不到的事:
|
||||
```markdown
|
||||
[CAPABILITY EVAL: feature-name]
|
||||
任務:Claude 應完成什麼的描述
|
||||
成功標準:
|
||||
- [ ] 標準 1
|
||||
- [ ] 標準 2
|
||||
- [ ] 標準 3
|
||||
預期輸出:預期結果描述
|
||||
```
|
||||
|
||||
### 回歸 Evals
|
||||
確保變更不會破壞現有功能:
|
||||
```markdown
|
||||
[REGRESSION EVAL: feature-name]
|
||||
基準:SHA 或檢查點名稱
|
||||
測試:
|
||||
- existing-test-1: PASS/FAIL
|
||||
- existing-test-2: PASS/FAIL
|
||||
- existing-test-3: PASS/FAIL
|
||||
結果:X/Y 通過(先前為 Y/Y)
|
||||
```
|
||||
|
||||
## 評分器類型
|
||||
|
||||
### 1. 基於程式碼的評分器
|
||||
使用程式碼的確定性檢查:
|
||||
```bash
|
||||
# 檢查檔案是否包含預期模式
|
||||
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
|
||||
|
||||
# 檢查測試是否通過
|
||||
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
|
||||
|
||||
# 檢查建置是否成功
|
||||
npm run build && echo "PASS" || echo "FAIL"
|
||||
```
|
||||
|
||||
### 2. 基於模型的評分器
|
||||
使用 Claude 評估開放式輸出:
|
||||
```markdown
|
||||
[MODEL GRADER PROMPT]
|
||||
評估以下程式碼變更:
|
||||
1. 它是否解決了陳述的問題?
|
||||
2. 結構是否良好?
|
||||
3. 邊界案例是否被處理?
|
||||
4. 錯誤處理是否適當?
|
||||
|
||||
分數:1-5(1=差,5=優秀)
|
||||
理由:[解釋]
|
||||
```
|
||||
|
||||
### 3. 人工評分器
|
||||
標記為手動審查:
|
||||
```markdown
|
||||
[HUMAN REVIEW REQUIRED]
|
||||
變更:變更內容的描述
|
||||
理由:為何需要人工審查
|
||||
風險等級:LOW/MEDIUM/HIGH
|
||||
```
|
||||
|
||||
## 指標
|
||||
|
||||
### pass@k
|
||||
「k 次嘗試中至少一次成功」
|
||||
- pass@1:第一次嘗試成功率
|
||||
- pass@3:3 次嘗試內成功
|
||||
- 典型目標:pass@3 > 90%
|
||||
|
||||
### pass^k
|
||||
「所有 k 次試驗都成功」
|
||||
- 更高的可靠性標準
|
||||
- pass^3:連續 3 次成功
|
||||
- 用於關鍵路徑
|
||||
|
||||
## Eval 工作流程
|
||||
|
||||
### 1. 定義(編碼前)
|
||||
```markdown
|
||||
## EVAL 定義:feature-xyz
|
||||
|
||||
### 能力 Evals
|
||||
1. 可以建立新使用者帳戶
|
||||
2. 可以驗證電子郵件格式
|
||||
3. 可以安全地雜湊密碼
|
||||
|
||||
### 回歸 Evals
|
||||
1. 現有登入仍可運作
|
||||
2. 工作階段管理未變更
|
||||
3. 登出流程完整
|
||||
|
||||
### 成功指標
|
||||
- 能力 evals 的 pass@3 > 90%
|
||||
- 回歸 evals 的 pass^3 = 100%
|
||||
```
|
||||
|
||||
### 2. 實作
|
||||
撰寫程式碼以通過定義的 evals。
|
||||
|
||||
### 3. 評估
|
||||
```bash
|
||||
# 執行能力 evals
|
||||
[執行每個能力 eval,記錄 PASS/FAIL]
|
||||
|
||||
# 執行回歸 evals
|
||||
npm test -- --testPathPattern="existing"
|
||||
|
||||
# 產生報告
|
||||
```
|
||||
|
||||
### 4. 報告
|
||||
```markdown
|
||||
EVAL 報告:feature-xyz
|
||||
========================
|
||||
|
||||
能力 Evals:
|
||||
create-user: PASS (pass@1)
|
||||
validate-email: PASS (pass@2)
|
||||
hash-password: PASS (pass@1)
|
||||
整體: 3/3 通過
|
||||
|
||||
回歸 Evals:
|
||||
login-flow: PASS
|
||||
session-mgmt: PASS
|
||||
logout-flow: PASS
|
||||
整體: 3/3 通過
|
||||
|
||||
指標:
|
||||
pass@1: 67% (2/3)
|
||||
pass@3: 100% (3/3)
|
||||
|
||||
狀態:準備審查
|
||||
```
|
||||
|
||||
## 整合模式
|
||||
|
||||
### 實作前
|
||||
```
|
||||
/eval define feature-name
|
||||
```
|
||||
在 `.claude/evals/feature-name.md` 建立 eval 定義檔案
|
||||
|
||||
### 實作期間
|
||||
```
|
||||
/eval check feature-name
|
||||
```
|
||||
執行當前 evals 並報告狀態
|
||||
|
||||
### 實作後
|
||||
```
|
||||
/eval report feature-name
|
||||
```
|
||||
產生完整 eval 報告
|
||||
|
||||
## Eval 儲存
|
||||
|
||||
在專案中儲存 evals:
|
||||
```
|
||||
.claude/
|
||||
evals/
|
||||
feature-xyz.md # Eval 定義
|
||||
feature-xyz.log # Eval 執行歷史
|
||||
baseline.json # 回歸基準
|
||||
```
|
||||
|
||||
## 最佳實務
|
||||
|
||||
1. **編碼前定義 evals** - 強制清楚思考成功標準
|
||||
2. **頻繁執行 evals** - 及早捕捉回歸
|
||||
3. **隨時間追蹤 pass@k** - 監控可靠性趨勢
|
||||
4. **可能時使用程式碼評分器** - 確定性 > 機率性
|
||||
5. **安全性需人工審查** - 永遠不要完全自動化安全檢查
|
||||
6. **保持 evals 快速** - 慢 evals 不會被執行
|
||||
7. **與程式碼一起版本化 evals** - Evals 是一等工件
|
||||
|
||||
## 範例:新增認證
|
||||
|
||||
```markdown
|
||||
## EVAL:add-authentication
|
||||
|
||||
### 階段 1:定義(10 分鐘)
|
||||
能力 Evals:
|
||||
- [ ] 使用者可以用電子郵件/密碼註冊
|
||||
- [ ] 使用者可以用有效憑證登入
|
||||
- [ ] 無效憑證被拒絕並顯示適當錯誤
|
||||
- [ ] 工作階段在頁面重新載入後持續
|
||||
- [ ] 登出清除工作階段
|
||||
|
||||
回歸 Evals:
|
||||
- [ ] 公開路由仍可存取
|
||||
- [ ] API 回應未變更
|
||||
- [ ] 資料庫 schema 相容
|
||||
|
||||
### 階段 2:實作(視情況而定)
|
||||
[撰寫程式碼]
|
||||
|
||||
### 階段 3:評估
|
||||
執行:/eval check add-authentication
|
||||
|
||||
### 階段 4:報告
|
||||
EVAL 報告:add-authentication
|
||||
==============================
|
||||
能力:5/5 通過(pass@3:100%)
|
||||
回歸:3/3 通過(pass^3:100%)
|
||||
狀態:準備發佈
|
||||
```
|
||||
@@ -1,631 +0,0 @@
|
||||
---
|
||||
name: frontend-patterns
|
||||
description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
|
||||
---
|
||||
|
||||
# 前端開發模式
|
||||
|
||||
用於 React、Next.js 和高效能使用者介面的現代前端模式。
|
||||
|
||||
## 元件模式
|
||||
|
||||
### 組合優於繼承
|
||||
|
||||
```typescript
|
||||
// ✅ 良好:元件組合
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'outlined'
|
||||
}
|
||||
|
||||
export function Card({ children, variant = 'default' }: CardProps) {
|
||||
return <div className={`card card-${variant}`}>{children}</div>
|
||||
}
|
||||
|
||||
export function CardHeader({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-header">{children}</div>
|
||||
}
|
||||
|
||||
export function CardBody({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-body">{children}</div>
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
<Card>
|
||||
<CardHeader>標題</CardHeader>
|
||||
<CardBody>內容</CardBody>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### 複合元件
|
||||
|
||||
```typescript
|
||||
interface TabsContextValue {
|
||||
activeTab: string
|
||||
setActiveTab: (tab: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
||||
|
||||
export function Tabs({ children, defaultTab }: {
|
||||
children: React.ReactNode
|
||||
defaultTab: string
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState(defaultTab)
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function TabList({ children }: { children: React.ReactNode }) {
|
||||
return <div className="tab-list">{children}</div>
|
||||
}
|
||||
|
||||
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
||||
const context = useContext(TabsContext)
|
||||
if (!context) throw new Error('Tab must be used within Tabs')
|
||||
|
||||
return (
|
||||
<button
|
||||
className={context.activeTab === id ? 'active' : ''}
|
||||
onClick={() => context.setActiveTab(id)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
<Tabs defaultTab="overview">
|
||||
<TabList>
|
||||
<Tab id="overview">概覽</Tab>
|
||||
<Tab id="details">詳情</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
### Render Props 模式
|
||||
|
||||
```typescript
|
||||
interface DataLoaderProps<T> {
|
||||
url: string
|
||||
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
|
||||
}
|
||||
|
||||
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false))
|
||||
}, [url])
|
||||
|
||||
return <>{children(data, loading, error)}</>
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
<DataLoader<Market[]> url="/api/markets">
|
||||
{(markets, loading, error) => {
|
||||
if (loading) return <Spinner />
|
||||
if (error) return <Error error={error} />
|
||||
return <MarketList markets={markets!} />
|
||||
}}
|
||||
</DataLoader>
|
||||
```
|
||||
|
||||
## 自訂 Hooks 模式
|
||||
|
||||
### 狀態管理 Hook
|
||||
|
||||
```typescript
|
||||
export function useToggle(initialValue = false): [boolean, () => void] {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setValue(v => !v)
|
||||
}, [])
|
||||
|
||||
return [value, toggle]
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const [isOpen, toggleOpen] = useToggle()
|
||||
```
|
||||
|
||||
### 非同步資料取得 Hook
|
||||
|
||||
```typescript
|
||||
interface UseQueryOptions<T> {
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useQuery<T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>,
|
||||
options?: UseQueryOptions<T>
|
||||
) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await fetcher()
|
||||
setData(result)
|
||||
options?.onSuccess?.(result)
|
||||
} catch (err) {
|
||||
const error = err as Error
|
||||
setError(error)
|
||||
options?.onError?.(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fetcher, options])
|
||||
|
||||
useEffect(() => {
|
||||
if (options?.enabled !== false) {
|
||||
refetch()
|
||||
}
|
||||
}, [key, refetch, options?.enabled])
|
||||
|
||||
return { data, error, loading, refetch }
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const { data: markets, loading, error, refetch } = useQuery(
|
||||
'markets',
|
||||
() => fetch('/api/markets').then(r => r.json()),
|
||||
{
|
||||
onSuccess: data => console.log('Fetched', data.length, 'markets'),
|
||||
onError: err => console.error('Failed:', err)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Debounce Hook
|
||||
|
||||
```typescript
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery) {
|
||||
performSearch(debouncedQuery)
|
||||
}
|
||||
}, [debouncedQuery])
|
||||
```
|
||||
|
||||
## 狀態管理模式
|
||||
|
||||
### Context + Reducer 模式
|
||||
|
||||
```typescript
|
||||
interface State {
|
||||
markets: Market[]
|
||||
selectedMarket: Market | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_MARKETS'; payload: Market[] }
|
||||
| { type: 'SELECT_MARKET'; payload: Market }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'SET_MARKETS':
|
||||
return { ...state, markets: action.payload }
|
||||
case 'SELECT_MARKET':
|
||||
return { ...state, selectedMarket: action.payload }
|
||||
case 'SET_LOADING':
|
||||
return { ...state, loading: action.payload }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const MarketContext = createContext<{
|
||||
state: State
|
||||
dispatch: Dispatch<Action>
|
||||
} | undefined>(undefined)
|
||||
|
||||
export function MarketProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
markets: [],
|
||||
selectedMarket: null,
|
||||
loading: false
|
||||
})
|
||||
|
||||
return (
|
||||
<MarketContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</MarketContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useMarkets() {
|
||||
const context = useContext(MarketContext)
|
||||
if (!context) throw new Error('useMarkets must be used within MarketProvider')
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
## 效能優化
|
||||
|
||||
### 記憶化
|
||||
|
||||
```typescript
|
||||
// ✅ useMemo 用於昂貴計算
|
||||
const sortedMarkets = useMemo(() => {
|
||||
return markets.sort((a, b) => b.volume - a.volume)
|
||||
}, [markets])
|
||||
|
||||
// ✅ useCallback 用於傳遞給子元件的函式
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query)
|
||||
}, [])
|
||||
|
||||
// ✅ React.memo 用於純元件
|
||||
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
|
||||
return (
|
||||
<div className="market-card">
|
||||
<h3>{market.name}</h3>
|
||||
<p>{market.description}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
### 程式碼分割與延遲載入
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// ✅ 延遲載入重型元件
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<ChartSkeleton />}>
|
||||
<HeavyChart data={data} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<ThreeJsBackground />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 長列表虛擬化
|
||||
|
||||
```typescript
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
|
||||
export function VirtualMarketList({ markets }: { markets: Market[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: markets.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 100, // 預估行高
|
||||
overscan: 5 // 額外渲染的項目數
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
>
|
||||
<MarketCard market={markets[virtualRow.index]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 表單處理模式
|
||||
|
||||
### 帶驗證的受控表單
|
||||
|
||||
```typescript
|
||||
interface FormData {
|
||||
name: string
|
||||
description: string
|
||||
endDate: string
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
name?: string
|
||||
description?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export function CreateMarketForm() {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
endDate: ''
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
|
||||
const validate = (): boolean => {
|
||||
const newErrors: FormErrors = {}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = '名稱為必填'
|
||||
} else if (formData.name.length > 200) {
|
||||
newErrors.name = '名稱必須少於 200 個字元'
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
newErrors.description = '描述為必填'
|
||||
}
|
||||
|
||||
if (!formData.endDate) {
|
||||
newErrors.endDate = '結束日期為必填'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!validate()) return
|
||||
|
||||
try {
|
||||
await createMarket(formData)
|
||||
// 成功處理
|
||||
} catch (error) {
|
||||
// 錯誤處理
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={formData.name}
|
||||
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="市場名稱"
|
||||
/>
|
||||
{errors.name && <span className="error">{errors.name}</span>}
|
||||
|
||||
{/* 其他欄位 */}
|
||||
|
||||
<button type="submit">建立市場</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundary 模式
|
||||
|
||||
```typescript
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false,
|
||||
error: null
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Error boundary caught:', error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="error-fallback">
|
||||
<h2>發生錯誤</h2>
|
||||
<p>{this.state.error?.message}</p>
|
||||
<button onClick={() => this.setState({ hasError: false })}>
|
||||
重試
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
## 動畫模式
|
||||
|
||||
### Framer Motion 動畫
|
||||
|
||||
```typescript
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
// ✅ 列表動畫
|
||||
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{markets.map(market => (
|
||||
<motion.div
|
||||
key={market.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<MarketCard market={market} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ Modal 動畫
|
||||
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
className="modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<motion.div
|
||||
className="modal-content"
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 無障礙模式
|
||||
|
||||
### 鍵盤導航
|
||||
|
||||
```typescript
|
||||
export function Dropdown({ options, onSelect }: DropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setActiveIndex(i => Math.min(i + 1, options.length - 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setActiveIndex(i => Math.max(i - 1, 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
onSelect(options[activeIndex])
|
||||
setIsOpen(false)
|
||||
break
|
||||
case 'Escape':
|
||||
setIsOpen(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="combobox"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* 下拉選單實作 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 焦點管理
|
||||
|
||||
```typescript
|
||||
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// 儲存目前聚焦的元素
|
||||
previousFocusRef.current = document.activeElement as HTMLElement
|
||||
|
||||
// 聚焦 modal
|
||||
modalRef.current?.focus()
|
||||
} else {
|
||||
// 關閉時恢復焦點
|
||||
previousFocusRef.current?.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return isOpen ? (
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabIndex={-1}
|
||||
onKeyDown={e => e.key === 'Escape' && onClose()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
```
|
||||
|
||||
**記住**:現代前端模式能實現可維護、高效能的使用者介面。選擇符合你專案複雜度的模式。
|
||||
@@ -1,673 +0,0 @@
|
||||
---
|
||||
name: golang-patterns
|
||||
description: Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.
|
||||
---
|
||||
|
||||
# Go 開發模式
|
||||
|
||||
用於建構穩健、高效且可維護應用程式的慣用 Go 模式和最佳實務。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 撰寫新的 Go 程式碼
|
||||
- 審查 Go 程式碼
|
||||
- 重構現有 Go 程式碼
|
||||
- 設計 Go 套件/模組
|
||||
|
||||
## 核心原則
|
||||
|
||||
### 1. 簡單與清晰
|
||||
|
||||
Go 偏好簡單而非聰明。程式碼應該明顯且易讀。
|
||||
|
||||
```go
|
||||
// 良好:清晰直接
|
||||
func GetUser(id string) (*User, error) {
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user %s: %w", id, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 不良:過於聰明
|
||||
func GetUser(id string) (*User, error) {
|
||||
return func() (*User, error) {
|
||||
if u, e := db.FindUser(id); e == nil {
|
||||
return u, nil
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 讓零值有用
|
||||
|
||||
設計類型使其零值無需初始化即可立即使用。
|
||||
|
||||
```go
|
||||
// 良好:零值有用
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int // 零值為 0,可直接使用
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// 良好:bytes.Buffer 零值可用
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("hello")
|
||||
|
||||
// 不良:需要初始化
|
||||
type BadCounter struct {
|
||||
counts map[string]int // nil map 會 panic
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 接受介面,回傳結構
|
||||
|
||||
函式應接受介面參數並回傳具體類型。
|
||||
|
||||
```go
|
||||
// 良好:接受介面,回傳具體類型
|
||||
func ProcessData(r io.Reader) (*Result, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Data: data}, nil
|
||||
}
|
||||
|
||||
// 不良:回傳介面(不必要地隱藏實作細節)
|
||||
func ProcessData(r io.Reader) (io.Reader, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 錯誤處理模式
|
||||
|
||||
### 帶上下文的錯誤包裝
|
||||
|
||||
```go
|
||||
// 良好:包裝錯誤並加上上下文
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load config %s: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 自訂錯誤類型
|
||||
|
||||
```go
|
||||
// 定義領域特定錯誤
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// 常見情況的哨兵錯誤
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
```
|
||||
|
||||
### 使用 errors.Is 和 errors.As 檢查錯誤
|
||||
|
||||
```go
|
||||
func HandleError(err error) {
|
||||
// 檢查特定錯誤
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
log.Println("No records found")
|
||||
return
|
||||
}
|
||||
|
||||
// 檢查錯誤類型
|
||||
var validationErr *ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
log.Printf("Validation error on field %s: %s",
|
||||
validationErr.Field, validationErr.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// 未知錯誤
|
||||
log.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 絕不忽略錯誤
|
||||
|
||||
```go
|
||||
// 不良:用空白識別符忽略錯誤
|
||||
result, _ := doSomething()
|
||||
|
||||
// 良好:處理或明確說明為何安全忽略
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 可接受:當錯誤真的不重要時(罕見)
|
||||
_ = writer.Close() // 盡力清理,錯誤在其他地方記錄
|
||||
```
|
||||
|
||||
## 並行模式
|
||||
|
||||
### Worker Pool
|
||||
|
||||
```go
|
||||
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for job := range jobs {
|
||||
results <- process(job)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}
|
||||
```
|
||||
|
||||
### 取消和逾時的 Context
|
||||
|
||||
```go
|
||||
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
```
|
||||
|
||||
### 優雅關閉
|
||||
|
||||
```go
|
||||
func GracefulShutdown(server *http.Server) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}
|
||||
```
|
||||
|
||||
### 協調 Goroutines 的 errgroup
|
||||
|
||||
```go
|
||||
import "golang.org/x/sync/errgroup"
|
||||
|
||||
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
results := make([][]byte, len(urls))
|
||||
|
||||
for i, url := range urls {
|
||||
i, url := i, url // 捕獲迴圈變數
|
||||
g.Go(func() error {
|
||||
data, err := FetchWithTimeout(ctx, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results[i] = data
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 避免 Goroutine 洩漏
|
||||
|
||||
```go
|
||||
// 不良:如果 context 被取消會洩漏 goroutine
|
||||
func leakyFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte)
|
||||
go func() {
|
||||
data, _ := fetch(url)
|
||||
ch <- data // 如果無接收者會永遠阻塞
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// 良好:正確處理取消
|
||||
func safeFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte, 1) // 帶緩衝的 channel
|
||||
go func() {
|
||||
data, err := fetch(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- data:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
```
|
||||
|
||||
## 介面設計
|
||||
|
||||
### 小而專注的介面
|
||||
|
||||
```go
|
||||
// 良好:單一方法介面
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// 依需要組合介面
|
||||
type ReadWriteCloser interface {
|
||||
Reader
|
||||
Writer
|
||||
Closer
|
||||
}
|
||||
```
|
||||
|
||||
### 在使用處定義介面
|
||||
|
||||
```go
|
||||
// 在消費者套件中,而非提供者
|
||||
package service
|
||||
|
||||
// UserStore 定義此服務需要的內容
|
||||
type UserStore interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store UserStore
|
||||
}
|
||||
|
||||
// 具體實作可以在另一個套件
|
||||
// 它不需要知道這個介面
|
||||
```
|
||||
|
||||
### 使用型別斷言的可選行為
|
||||
|
||||
```go
|
||||
type Flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
func WriteAndFlush(w io.Writer, data []byte) error {
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果支援則 Flush
|
||||
if f, ok := w.(Flusher); ok {
|
||||
return f.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 套件組織
|
||||
|
||||
### 標準專案結構
|
||||
|
||||
```text
|
||||
myproject/
|
||||
├── cmd/
|
||||
│ └── myapp/
|
||||
│ └── main.go # 進入點
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handlers
|
||||
│ ├── service/ # 業務邏輯
|
||||
│ ├── repository/ # 資料存取
|
||||
│ └── config/ # 設定
|
||||
├── pkg/
|
||||
│ └── client/ # 公開 API 客戶端
|
||||
├── api/
|
||||
│ └── v1/ # API 定義(proto、OpenAPI)
|
||||
├── testdata/ # 測試 fixtures
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
### 套件命名
|
||||
|
||||
```go
|
||||
// 良好:簡短、小寫、無底線
|
||||
package http
|
||||
package json
|
||||
package user
|
||||
|
||||
// 不良:冗長、混合大小寫或冗餘
|
||||
package httpHandler
|
||||
package json_parser
|
||||
package userService // 冗餘的 'Service' 後綴
|
||||
```
|
||||
|
||||
### 避免套件層級狀態
|
||||
|
||||
```go
|
||||
// 不良:全域可變狀態
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||
}
|
||||
|
||||
// 良好:依賴注入
|
||||
type Server struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB) *Server {
|
||||
return &Server{db: db}
|
||||
}
|
||||
```
|
||||
|
||||
## 結構設計
|
||||
|
||||
### Functional Options 模式
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
addr string
|
||||
timeout time.Duration
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Option func(*Server)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Server) {
|
||||
s.timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(l *log.Logger) Option {
|
||||
return func(s *Server) {
|
||||
s.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(addr string, opts ...Option) *Server {
|
||||
s := &Server{
|
||||
addr: addr,
|
||||
timeout: 30 * time.Second, // 預設值
|
||||
logger: log.Default(), // 預設值
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
server := NewServer(":8080",
|
||||
WithTimeout(60*time.Second),
|
||||
WithLogger(customLogger),
|
||||
)
|
||||
```
|
||||
|
||||
### 嵌入用於組合
|
||||
|
||||
```go
|
||||
type Logger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (l *Logger) Log(msg string) {
|
||||
fmt.Printf("[%s] %s\n", l.prefix, msg)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
*Logger // 嵌入 - Server 獲得 Log 方法
|
||||
addr string
|
||||
}
|
||||
|
||||
func NewServer(addr string) *Server {
|
||||
return &Server{
|
||||
Logger: &Logger{prefix: "SERVER"},
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
s := NewServer(":8080")
|
||||
s.Log("Starting...") // 呼叫嵌入的 Logger.Log
|
||||
```
|
||||
|
||||
## 記憶體與效能
|
||||
|
||||
### 已知大小時預分配 Slice
|
||||
|
||||
```go
|
||||
// 不良:多次擴展 slice
|
||||
func processItems(items []Item) []Result {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// 良好:單次分配
|
||||
func processItems(items []Item) []Result {
|
||||
results := make([]Result, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### 頻繁分配使用 sync.Pool
|
||||
|
||||
```go
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessRequest(data []byte) []byte {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
// 處理...
|
||||
return buf.Bytes()
|
||||
}
|
||||
```
|
||||
|
||||
### 避免迴圈中的字串串接
|
||||
|
||||
```go
|
||||
// 不良:產生多次字串分配
|
||||
func join(parts []string) string {
|
||||
var result string
|
||||
for _, p := range parts {
|
||||
result += p + ","
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 良好:使用 strings.Builder 單次分配
|
||||
func join(parts []string) string {
|
||||
var sb strings.Builder
|
||||
for i, p := range parts {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// 最佳:使用標準函式庫
|
||||
func join(parts []string) string {
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
```
|
||||
|
||||
## Go 工具整合
|
||||
|
||||
### 基本指令
|
||||
|
||||
```bash
|
||||
# 建置和執行
|
||||
go build ./...
|
||||
go run ./cmd/myapp
|
||||
|
||||
# 測試
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
go test -cover ./...
|
||||
|
||||
# 靜態分析
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
golangci-lint run
|
||||
|
||||
# 模組管理
|
||||
go mod tidy
|
||||
go mod verify
|
||||
|
||||
# 格式化
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
```
|
||||
|
||||
### 建議的 Linter 設定(.golangci.yml)
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- gofmt
|
||||
- goimports
|
||||
- misspell
|
||||
- unconvert
|
||||
- unparam
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
govet:
|
||||
check-shadowing: true
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
```
|
||||
|
||||
## 快速參考:Go 慣用語
|
||||
|
||||
| 慣用語 | 描述 |
|
||||
|-------|------|
|
||||
| 接受介面,回傳結構 | 函式接受介面參數,回傳具體類型 |
|
||||
| 錯誤是值 | 將錯誤視為一等值,而非例外 |
|
||||
| 不要透過共享記憶體通訊 | 使用 channel 在 goroutine 間協調 |
|
||||
| 讓零值有用 | 類型應無需明確初始化即可工作 |
|
||||
| 一點複製比一點依賴好 | 避免不必要的外部依賴 |
|
||||
| 清晰優於聰明 | 優先考慮可讀性而非聰明 |
|
||||
| gofmt 不是任何人的最愛但是所有人的朋友 | 總是用 gofmt/goimports 格式化 |
|
||||
| 提早返回 | 先處理錯誤,保持快樂路徑不縮排 |
|
||||
|
||||
## 要避免的反模式
|
||||
|
||||
```go
|
||||
// 不良:長函式中的裸返回
|
||||
func process() (result int, err error) {
|
||||
// ... 50 行 ...
|
||||
return // 返回什麼?
|
||||
}
|
||||
|
||||
// 不良:使用 panic 作為控制流程
|
||||
func GetUser(id string) *User {
|
||||
user, err := db.Find(id)
|
||||
if err != nil {
|
||||
panic(err) // 不要這樣做
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// 不良:在結構中傳遞 context
|
||||
type Request struct {
|
||||
ctx context.Context // Context 應該是第一個參數
|
||||
ID string
|
||||
}
|
||||
|
||||
// 良好:Context 作為第一個參數
|
||||
func ProcessRequest(ctx context.Context, id string) error {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 不良:混合值和指標接收器
|
||||
type Counter struct{ n int }
|
||||
func (c Counter) Value() int { return c.n } // 值接收器
|
||||
func (c *Counter) Increment() { c.n++ } // 指標接收器
|
||||
// 選擇一種風格並保持一致
|
||||
```
|
||||
|
||||
**記住**:Go 程式碼應該以最好的方式無聊 - 可預測、一致且易於理解。有疑慮時,保持簡單。
|
||||
@@ -1,710 +0,0 @@
|
||||
---
|
||||
name: golang-testing
|
||||
description: Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices.
|
||||
---
|
||||
|
||||
# Go 測試模式
|
||||
|
||||
用於撰寫可靠、可維護測試的完整 Go 測試模式,遵循 TDD 方法論。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 撰寫新的 Go 函式或方法
|
||||
- 為現有程式碼增加測試覆蓋率
|
||||
- 為效能關鍵程式碼建立基準測試
|
||||
- 實作輸入驗證的模糊測試
|
||||
- 在 Go 專案中遵循 TDD 工作流程
|
||||
|
||||
## Go 的 TDD 工作流程
|
||||
|
||||
### RED-GREEN-REFACTOR 循環
|
||||
|
||||
```
|
||||
RED → 先寫失敗的測試
|
||||
GREEN → 撰寫最少程式碼使測試通過
|
||||
REFACTOR → 在保持測試綠色的同時改善程式碼
|
||||
REPEAT → 繼續下一個需求
|
||||
```
|
||||
|
||||
### Go 中的逐步 TDD
|
||||
|
||||
```go
|
||||
// 步驟 1:定義介面/簽章
|
||||
// calculator.go
|
||||
package calculator
|
||||
|
||||
func Add(a, b int) int {
|
||||
panic("not implemented") // 佔位符
|
||||
}
|
||||
|
||||
// 步驟 2:撰寫失敗測試(RED)
|
||||
// calculator_test.go
|
||||
package calculator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
got := Add(2, 3)
|
||||
want := 5
|
||||
if got != want {
|
||||
t.Errorf("Add(2, 3) = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 步驟 3:執行測試 - 驗證失敗
|
||||
// $ go test
|
||||
// --- FAIL: TestAdd (0.00s)
|
||||
// panic: not implemented
|
||||
|
||||
// 步驟 4:實作最少程式碼(GREEN)
|
||||
func Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// 步驟 5:執行測試 - 驗證通過
|
||||
// $ go test
|
||||
// PASS
|
||||
|
||||
// 步驟 6:如需要則重構,驗證測試仍然通過
|
||||
```
|
||||
|
||||
## 表格驅動測試
|
||||
|
||||
Go 測試的標準模式。以最少程式碼達到完整覆蓋。
|
||||
|
||||
```go
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 2, 3, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
{"zero values", 0, 0, 0},
|
||||
{"mixed signs", -1, 1, 0},
|
||||
{"large numbers", 1000000, 2000000, 3000000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Add(tt.a, tt.b)
|
||||
if got != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 帶錯誤案例的表格驅動測試
|
||||
|
||||
```go
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
input: `{"host": "localhost", "port": 8080}`,
|
||||
want: &Config{Host: "localhost", Port: 8080},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
input: `{invalid}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minimal config",
|
||||
input: `{}`,
|
||||
want: &Config{}, // 零值 config
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseConfig(tt.input)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %+v; want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 子測試
|
||||
|
||||
### 組織相關測試
|
||||
|
||||
```go
|
||||
func TestUser(t *testing.T) {
|
||||
// 所有子測試共享的設置
|
||||
db := setupTestDB(t)
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
user := &User{Name: "Alice"}
|
||||
err := db.CreateUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser failed: %v", err)
|
||||
}
|
||||
if user.ID == "" {
|
||||
t.Error("expected user ID to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
user, err := db.GetUser("alice-id")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser failed: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 並行子測試
|
||||
|
||||
```go
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"case1", "input1"},
|
||||
{"case2", "input2"},
|
||||
{"case3", "input3"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // 捕獲範圍變數
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // 並行執行子測試
|
||||
result := Process(tt.input)
|
||||
// 斷言...
|
||||
_ = result
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 測試輔助函式
|
||||
|
||||
### 輔助函式
|
||||
|
||||
```go
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper() // 標記為輔助函式
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
// 測試結束時清理
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
// 執行 migrations
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
t.Fatalf("failed to create schema: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func assertNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t *testing.T, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 臨時檔案和目錄
|
||||
|
||||
```go
|
||||
func TestFileProcessing(t *testing.T) {
|
||||
// 建立臨時目錄 - 自動清理
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 建立測試檔案
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// 執行測試
|
||||
result, err := ProcessFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessFile failed: %v", err)
|
||||
}
|
||||
|
||||
// 斷言...
|
||||
_ = result
|
||||
}
|
||||
```
|
||||
|
||||
## Golden 檔案
|
||||
|
||||
使用儲存在 `testdata/` 中的預期輸出檔案進行測試。
|
||||
|
||||
```go
|
||||
var update = flag.Bool("update", false, "update golden files")
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Template
|
||||
}{
|
||||
{"simple", Template{Name: "test"}},
|
||||
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Render(tt.input)
|
||||
|
||||
golden := filepath.Join("testdata", tt.name+".golden")
|
||||
|
||||
if *update {
|
||||
// 更新 golden 檔案:go test -update
|
||||
err := os.WriteFile(golden, got, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update golden file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
want, err := os.ReadFile(golden)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read golden file: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用介面 Mock
|
||||
|
||||
### 基於介面的 Mock
|
||||
|
||||
```go
|
||||
// 定義依賴的介面
|
||||
type UserRepository interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
// 生產實作
|
||||
type PostgresUserRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
|
||||
// 實際資料庫查詢
|
||||
}
|
||||
|
||||
// 測試用 Mock 實作
|
||||
type MockUserRepository struct {
|
||||
GetUserFunc func(id string) (*User, error)
|
||||
SaveUserFunc func(user *User) error
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetUser(id string) (*User, error) {
|
||||
return m.GetUserFunc(id)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) SaveUser(user *User) error {
|
||||
return m.SaveUserFunc(user)
|
||||
}
|
||||
|
||||
// 使用 mock 的測試
|
||||
func TestUserService(t *testing.T) {
|
||||
mock := &MockUserRepository{
|
||||
GetUserFunc: func(id string) (*User, error) {
|
||||
if id == "123" {
|
||||
return &User{ID: "123", Name: "Alice"}, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
|
||||
user, err := service.GetUserProfile("123")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基準測試
|
||||
|
||||
### 基本基準測試
|
||||
|
||||
```go
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := generateTestData(1000)
|
||||
b.ResetTimer() // 不計算設置時間
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 執行:go test -bench=BenchmarkProcess -benchmem
|
||||
// 輸出:BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
|
||||
```
|
||||
|
||||
### 不同大小的基準測試
|
||||
|
||||
```go
|
||||
func BenchmarkSort(b *testing.B) {
|
||||
sizes := []int{100, 1000, 10000, 100000}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
data := generateRandomSlice(size)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// 複製以避免排序已排序的資料
|
||||
tmp := make([]int, len(data))
|
||||
copy(tmp, data)
|
||||
sort.Ints(tmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 記憶體分配基準測試
|
||||
|
||||
```go
|
||||
func BenchmarkStringConcat(b *testing.B) {
|
||||
parts := []string{"hello", "world", "foo", "bar", "baz"}
|
||||
|
||||
b.Run("plus", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var s string
|
||||
for _, p := range parts {
|
||||
s += p
|
||||
}
|
||||
_ = s
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("builder", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var sb strings.Builder
|
||||
for _, p := range parts {
|
||||
sb.WriteString(p)
|
||||
}
|
||||
_ = sb.String()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("join", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.Join(parts, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 模糊測試(Go 1.18+)
|
||||
|
||||
### 基本模糊測試
|
||||
|
||||
```go
|
||||
func FuzzParseJSON(f *testing.F) {
|
||||
// 新增種子語料庫
|
||||
f.Add(`{"name": "test"}`)
|
||||
f.Add(`{"count": 123}`)
|
||||
f.Add(`[]`)
|
||||
f.Add(`""`)
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal([]byte(input), &result)
|
||||
|
||||
if err != nil {
|
||||
// 隨機輸入預期會有無效 JSON
|
||||
return
|
||||
}
|
||||
|
||||
// 如果解析成功,重新編碼應該可行
|
||||
_, err = json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 執行:go test -fuzz=FuzzParseJSON -fuzztime=30s
|
||||
```
|
||||
|
||||
### 多輸入模糊測試
|
||||
|
||||
```go
|
||||
func FuzzCompare(f *testing.F) {
|
||||
f.Add("hello", "world")
|
||||
f.Add("", "")
|
||||
f.Add("abc", "abc")
|
||||
|
||||
f.Fuzz(func(t *testing.T, a, b string) {
|
||||
result := Compare(a, b)
|
||||
|
||||
// 屬性:Compare(a, a) 應該總是等於 0
|
||||
if a == b && result != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
|
||||
}
|
||||
|
||||
// 屬性:Compare(a, b) 和 Compare(b, a) 應該有相反符號
|
||||
reverse := Compare(b, a)
|
||||
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
|
||||
if result != 0 || reverse != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
|
||||
a, b, result, b, a, reverse)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 測試覆蓋率
|
||||
|
||||
### 執行覆蓋率
|
||||
|
||||
```bash
|
||||
# 基本覆蓋率
|
||||
go test -cover ./...
|
||||
|
||||
# 產生覆蓋率 profile
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# 在瀏覽器查看覆蓋率
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# 按函式查看覆蓋率
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
# 含競態偵測的覆蓋率
|
||||
go test -race -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
### 覆蓋率目標
|
||||
|
||||
| 程式碼類型 | 目標 |
|
||||
|-----------|------|
|
||||
| 關鍵業務邏輯 | 100% |
|
||||
| 公開 API | 90%+ |
|
||||
| 一般程式碼 | 80%+ |
|
||||
| 產生的程式碼 | 排除 |
|
||||
|
||||
## HTTP Handler 測試
|
||||
|
||||
```go
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
// 建立請求
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// 呼叫 handler
|
||||
HealthHandler(w, req)
|
||||
|
||||
// 檢查回應
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "OK" {
|
||||
t.Errorf("got body %q; want %q", body, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "get user",
|
||||
method: http.MethodGet,
|
||||
path: "/users/123",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"id":"123","name":"Alice"}`,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
method: http.MethodGet,
|
||||
path: "/users/999",
|
||||
wantStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "create user",
|
||||
method: http.MethodPost,
|
||||
path: "/users",
|
||||
body: `{"name":"Bob"}`,
|
||||
wantStatus: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAPIHandler()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body io.Reader
|
||||
if tt.body != "" {
|
||||
body = strings.NewReader(tt.body)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tt.method, tt.path, body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
|
||||
}
|
||||
|
||||
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
|
||||
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 測試指令
|
||||
|
||||
```bash
|
||||
# 執行所有測試
|
||||
go test ./...
|
||||
|
||||
# 執行詳細輸出的測試
|
||||
go test -v ./...
|
||||
|
||||
# 執行特定測試
|
||||
go test -run TestAdd ./...
|
||||
|
||||
# 執行匹配模式的測試
|
||||
go test -run "TestUser/Create" ./...
|
||||
|
||||
# 執行帶競態偵測器的測試
|
||||
go test -race ./...
|
||||
|
||||
# 執行帶覆蓋率的測試
|
||||
go test -cover -coverprofile=coverage.out ./...
|
||||
|
||||
# 只執行短測試
|
||||
go test -short ./...
|
||||
|
||||
# 執行帶逾時的測試
|
||||
go test -timeout 30s ./...
|
||||
|
||||
# 執行基準測試
|
||||
go test -bench=. -benchmem ./...
|
||||
|
||||
# 執行模糊測試
|
||||
go test -fuzz=FuzzParse -fuzztime=30s ./...
|
||||
|
||||
# 計算測試執行次數(用於偵測不穩定測試)
|
||||
go test -count=10 ./...
|
||||
```
|
||||
|
||||
## 最佳實務
|
||||
|
||||
**應該做的:**
|
||||
- 先寫測試(TDD)
|
||||
- 使用表格驅動測試以獲得完整覆蓋
|
||||
- 測試行為,而非實作
|
||||
- 在輔助函式中使用 `t.Helper()`
|
||||
- 對獨立測試使用 `t.Parallel()`
|
||||
- 用 `t.Cleanup()` 清理資源
|
||||
- 使用描述情境的有意義測試名稱
|
||||
|
||||
**不應該做的:**
|
||||
- 不要直接測試私有函式(透過公開 API 測試)
|
||||
- 不要在測試中使用 `time.Sleep()`(使用 channels 或條件)
|
||||
- 不要忽略不穩定測試(修復或移除它們)
|
||||
- 不要 mock 所有東西(可能時偏好整合測試)
|
||||
- 不要跳過錯誤路徑測試
|
||||
|
||||
## CI/CD 整合
|
||||
|
||||
```yaml
|
||||
# GitHub Actions 範例
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Run tests
|
||||
run: go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Check coverage
|
||||
run: |
|
||||
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
|
||||
awk -F'%' '{if ($1 < 80) exit 1}'
|
||||
```
|
||||
|
||||
**記住**:測試是文件。它們展示你的程式碼應該如何使用。清楚地撰寫並保持更新。
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
name: iterative-retrieval
|
||||
description: Pattern for progressively refining context retrieval to solve the subagent context problem
|
||||
---
|
||||
|
||||
# 迭代檢索模式
|
||||
|
||||
解決多 agent 工作流程中的「上下文問題」,其中子 agents 在開始工作之前不知道需要什麼上下文。
|
||||
|
||||
## 問題
|
||||
|
||||
子 agents 以有限上下文產生。它們不知道:
|
||||
- 哪些檔案包含相關程式碼
|
||||
- 程式碼庫中存在什麼模式
|
||||
- 專案使用什麼術語
|
||||
|
||||
標準方法失敗:
|
||||
- **傳送所有內容**:超過上下文限制
|
||||
- **不傳送內容**:Agent 缺乏關鍵資訊
|
||||
- **猜測需要什麼**:經常錯誤
|
||||
|
||||
## 解決方案:迭代檢索
|
||||
|
||||
一個漸進精煉上下文的 4 階段循環:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ DISPATCH │─────▶│ EVALUATE │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ ▲ │ │
|
||||
│ │ ▼ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ LOOP │◀─────│ REFINE │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ 最多 3 個循環,然後繼續 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 階段 1:DISPATCH
|
||||
|
||||
初始廣泛查詢以收集候選檔案:
|
||||
|
||||
```javascript
|
||||
// 從高層意圖開始
|
||||
const initialQuery = {
|
||||
patterns: ['src/**/*.ts', 'lib/**/*.ts'],
|
||||
keywords: ['authentication', 'user', 'session'],
|
||||
excludes: ['*.test.ts', '*.spec.ts']
|
||||
};
|
||||
|
||||
// 派遣到檢索 agent
|
||||
const candidates = await retrieveFiles(initialQuery);
|
||||
```
|
||||
|
||||
### 階段 2:EVALUATE
|
||||
|
||||
評估檢索內容的相關性:
|
||||
|
||||
```javascript
|
||||
function evaluateRelevance(files, task) {
|
||||
return files.map(file => ({
|
||||
path: file.path,
|
||||
relevance: scoreRelevance(file.content, task),
|
||||
reason: explainRelevance(file.content, task),
|
||||
missingContext: identifyGaps(file.content, task)
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
評分標準:
|
||||
- **高(0.8-1.0)**:直接實作目標功能
|
||||
- **中(0.5-0.7)**:包含相關模式或類型
|
||||
- **低(0.2-0.4)**:間接相關
|
||||
- **無(0-0.2)**:不相關,排除
|
||||
|
||||
### 階段 3:REFINE
|
||||
|
||||
基於評估更新搜尋標準:
|
||||
|
||||
```javascript
|
||||
function refineQuery(evaluation, previousQuery) {
|
||||
return {
|
||||
// 新增在高相關性檔案中發現的新模式
|
||||
patterns: [...previousQuery.patterns, ...extractPatterns(evaluation)],
|
||||
|
||||
// 新增在程式碼庫中找到的術語
|
||||
keywords: [...previousQuery.keywords, ...extractKeywords(evaluation)],
|
||||
|
||||
// 排除確認不相關的路徑
|
||||
excludes: [...previousQuery.excludes, ...evaluation
|
||||
.filter(e => e.relevance < 0.2)
|
||||
.map(e => e.path)
|
||||
],
|
||||
|
||||
// 針對特定缺口
|
||||
focusAreas: evaluation
|
||||
.flatMap(e => e.missingContext)
|
||||
.filter(unique)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 階段 4:LOOP
|
||||
|
||||
以精煉標準重複(最多 3 個循環):
|
||||
|
||||
```javascript
|
||||
async function iterativeRetrieve(task, maxCycles = 3) {
|
||||
let query = createInitialQuery(task);
|
||||
let bestContext = [];
|
||||
|
||||
for (let cycle = 0; cycle < maxCycles; cycle++) {
|
||||
const candidates = await retrieveFiles(query);
|
||||
const evaluation = evaluateRelevance(candidates, task);
|
||||
|
||||
// 檢查是否有足夠上下文
|
||||
const highRelevance = evaluation.filter(e => e.relevance >= 0.7);
|
||||
if (highRelevance.length >= 3 && !hasCriticalGaps(evaluation)) {
|
||||
return highRelevance;
|
||||
}
|
||||
|
||||
// 精煉並繼續
|
||||
query = refineQuery(evaluation, query);
|
||||
bestContext = mergeContext(bestContext, highRelevance);
|
||||
}
|
||||
|
||||
return bestContext;
|
||||
}
|
||||
```
|
||||
|
||||
## 實際範例
|
||||
|
||||
### 範例 1:Bug 修復上下文
|
||||
|
||||
```
|
||||
任務:「修復認證 token 過期 bug」
|
||||
|
||||
循環 1:
|
||||
DISPATCH:在 src/** 搜尋 "token"、"auth"、"expiry"
|
||||
EVALUATE:找到 auth.ts (0.9)、tokens.ts (0.8)、user.ts (0.3)
|
||||
REFINE:新增 "refresh"、"jwt" 關鍵字;排除 user.ts
|
||||
|
||||
循環 2:
|
||||
DISPATCH:搜尋精煉術語
|
||||
EVALUATE:找到 session-manager.ts (0.95)、jwt-utils.ts (0.85)
|
||||
REFINE:足夠上下文(2 個高相關性檔案)
|
||||
|
||||
結果:auth.ts、tokens.ts、session-manager.ts、jwt-utils.ts
|
||||
```
|
||||
|
||||
### 範例 2:功能實作
|
||||
|
||||
```
|
||||
任務:「為 API 端點增加速率限制」
|
||||
|
||||
循環 1:
|
||||
DISPATCH:在 routes/** 搜尋 "rate"、"limit"、"api"
|
||||
EVALUATE:無匹配 - 程式碼庫使用 "throttle" 術語
|
||||
REFINE:新增 "throttle"、"middleware" 關鍵字
|
||||
|
||||
循環 2:
|
||||
DISPATCH:搜尋精煉術語
|
||||
EVALUATE:找到 throttle.ts (0.9)、middleware/index.ts (0.7)
|
||||
REFINE:需要路由器模式
|
||||
|
||||
循環 3:
|
||||
DISPATCH:搜尋 "router"、"express" 模式
|
||||
EVALUATE:找到 router-setup.ts (0.8)
|
||||
REFINE:足夠上下文
|
||||
|
||||
結果:throttle.ts、middleware/index.ts、router-setup.ts
|
||||
```
|
||||
|
||||
## 與 Agents 整合
|
||||
|
||||
在 agent 提示中使用:
|
||||
|
||||
```markdown
|
||||
為此任務檢索上下文時:
|
||||
1. 從廣泛關鍵字搜尋開始
|
||||
2. 評估每個檔案的相關性(0-1 尺度)
|
||||
3. 識別仍缺少的上下文
|
||||
4. 精煉搜尋標準並重複(最多 3 個循環)
|
||||
5. 回傳相關性 >= 0.7 的檔案
|
||||
```
|
||||
|
||||
## 最佳實務
|
||||
|
||||
1. **從廣泛開始,逐漸縮小** - 不要過度指定初始查詢
|
||||
2. **學習程式碼庫術語** - 第一個循環通常會揭示命名慣例
|
||||
3. **追蹤缺失內容** - 明確的缺口識別驅動精煉
|
||||
4. **在「足夠好」時停止** - 3 個高相關性檔案勝過 10 個普通檔案
|
||||
5. **自信地排除** - 低相關性檔案不會變得相關
|
||||
|
||||
## 相關
|
||||
|
||||
- [Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 子 agent 協調章節
|
||||
- `continuous-learning` 技能 - 用於隨時間改進的模式
|
||||
- `~/.claude/agents/` 中的 Agent 定義
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
name: postgres-patterns
|
||||
description: PostgreSQL database patterns for query optimization, schema design, indexing, and security. Based on Supabase best practices.
|
||||
---
|
||||
|
||||
# PostgreSQL 模式
|
||||
|
||||
PostgreSQL 最佳實務快速參考。詳細指南請使用 `database-reviewer` agent。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 撰寫 SQL 查詢或 migrations
|
||||
- 設計資料庫 schema
|
||||
- 疑難排解慢查詢
|
||||
- 實作 Row Level Security
|
||||
- 設定連線池
|
||||
|
||||
## 快速參考
|
||||
|
||||
### 索引速查表
|
||||
|
||||
| 查詢模式 | 索引類型 | 範例 |
|
||||
|---------|---------|------|
|
||||
| `WHERE col = value` | B-tree(預設) | `CREATE INDEX idx ON t (col)` |
|
||||
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
|
||||
| `WHERE a = x AND b > y` | 複合 | `CREATE INDEX idx ON t (a, b)` |
|
||||
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||
| 時間序列範圍 | BRIN | `CREATE INDEX idx ON t USING brin (col)` |
|
||||
|
||||
### 資料類型快速參考
|
||||
|
||||
| 使用情況 | 正確類型 | 避免 |
|
||||
|---------|---------|------|
|
||||
| IDs | `bigint` | `int`、隨機 UUID |
|
||||
| 字串 | `text` | `varchar(255)` |
|
||||
| 時間戳 | `timestamptz` | `timestamp` |
|
||||
| 金額 | `numeric(10,2)` | `float` |
|
||||
| 旗標 | `boolean` | `varchar`、`int` |
|
||||
|
||||
### 常見模式
|
||||
|
||||
**複合索引順序:**
|
||||
```sql
|
||||
-- 等值欄位優先,然後是範圍欄位
|
||||
CREATE INDEX idx ON orders (status, created_at);
|
||||
-- 適用於:WHERE status = 'pending' AND created_at > '2024-01-01'
|
||||
```
|
||||
|
||||
**覆蓋索引:**
|
||||
```sql
|
||||
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
|
||||
-- 避免 SELECT email, name, created_at 時的表格查詢
|
||||
```
|
||||
|
||||
**部分索引:**
|
||||
```sql
|
||||
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
|
||||
-- 更小的索引,只包含活躍使用者
|
||||
```
|
||||
|
||||
**RLS 政策(優化):**
|
||||
```sql
|
||||
CREATE POLICY policy ON orders
|
||||
USING ((SELECT auth.uid()) = user_id); -- 用 SELECT 包裝!
|
||||
```
|
||||
|
||||
**UPSERT:**
|
||||
```sql
|
||||
INSERT INTO settings (user_id, key, value)
|
||||
VALUES (123, 'theme', 'dark')
|
||||
ON CONFLICT (user_id, key)
|
||||
DO UPDATE SET value = EXCLUDED.value;
|
||||
```
|
||||
|
||||
**游標分頁:**
|
||||
```sql
|
||||
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
|
||||
-- O(1) vs OFFSET 是 O(n)
|
||||
```
|
||||
|
||||
**佇列處理:**
|
||||
```sql
|
||||
UPDATE jobs SET status = 'processing'
|
||||
WHERE id = (
|
||||
SELECT id FROM jobs WHERE status = 'pending'
|
||||
ORDER BY created_at LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
) RETURNING *;
|
||||
```
|
||||
|
||||
### 反模式偵測
|
||||
|
||||
```sql
|
||||
-- 找出未建索引的外鍵
|
||||
SELECT conrelid::regclass, a.attname
|
||||
FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.contype = 'f'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_index i
|
||||
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
|
||||
);
|
||||
|
||||
-- 找出慢查詢
|
||||
SELECT query, mean_exec_time, calls
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_exec_time > 100
|
||||
ORDER BY mean_exec_time DESC;
|
||||
|
||||
-- 檢查表格膨脹
|
||||
SELECT relname, n_dead_tup, last_vacuum
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_dead_tup > 1000
|
||||
ORDER BY n_dead_tup DESC;
|
||||
```
|
||||
|
||||
### 設定範本
|
||||
|
||||
```sql
|
||||
-- 連線限制(依 RAM 調整)
|
||||
ALTER SYSTEM SET max_connections = 100;
|
||||
ALTER SYSTEM SET work_mem = '8MB';
|
||||
|
||||
-- 逾時
|
||||
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
|
||||
ALTER SYSTEM SET statement_timeout = '30s';
|
||||
|
||||
-- 監控
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
|
||||
-- 安全預設值
|
||||
REVOKE ALL ON SCHEMA public FROM public;
|
||||
|
||||
SELECT pg_reload_conf();
|
||||
```
|
||||
|
||||
## 相關
|
||||
|
||||
- Agent:`database-reviewer` - 完整資料庫審查工作流程
|
||||
- Skill:`clickhouse-io` - ClickHouse 分析模式
|
||||
- Skill:`backend-patterns` - API 和後端模式
|
||||
|
||||
---
|
||||
|
||||
*基於 [Supabase Agent Skills](https://github.com/supabase/agent-skills)(MIT 授權)*
|
||||
@@ -1,345 +0,0 @@
|
||||
# 專案指南技能(範例)
|
||||
|
||||
這是專案特定技能的範例。使用此作為你自己專案的範本。
|
||||
|
||||
基於真實生產應用程式:[Zenith](https://zenith.chat) - AI 驅動的客戶探索平台。
|
||||
|
||||
---
|
||||
|
||||
## 何時使用
|
||||
|
||||
在處理專案特定設計時參考此技能。專案技能包含:
|
||||
- 架構概覽
|
||||
- 檔案結構
|
||||
- 程式碼模式
|
||||
- 測試要求
|
||||
- 部署工作流程
|
||||
|
||||
---
|
||||
|
||||
## 架構概覽
|
||||
|
||||
**技術堆疊:**
|
||||
- **前端**:Next.js 15(App Router)、TypeScript、React
|
||||
- **後端**:FastAPI(Python)、Pydantic 模型
|
||||
- **資料庫**:Supabase(PostgreSQL)
|
||||
- **AI**:Claude API 帶工具呼叫和結構化輸出
|
||||
- **部署**:Google Cloud Run
|
||||
- **測試**:Playwright(E2E)、pytest(後端)、React Testing Library
|
||||
|
||||
**服務:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端 │
|
||||
│ Next.js 15 + TypeScript + TailwindCSS │
|
||||
│ 部署:Vercel / Cloud Run │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 後端 │
|
||||
│ FastAPI + Python 3.11 + Pydantic │
|
||||
│ 部署:Cloud Run │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Supabase │ │ Claude │ │ Redis │
|
||||
│ Database │ │ API │ │ Cache │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案結構
|
||||
|
||||
```
|
||||
project/
|
||||
├── frontend/
|
||||
│ └── src/
|
||||
│ ├── app/ # Next.js app router 頁面
|
||||
│ │ ├── api/ # API 路由
|
||||
│ │ ├── (auth)/ # 需認證路由
|
||||
│ │ └── workspace/ # 主應用程式工作區
|
||||
│ ├── components/ # React 元件
|
||||
│ │ ├── ui/ # 基礎 UI 元件
|
||||
│ │ ├── forms/ # 表單元件
|
||||
│ │ └── layouts/ # 版面配置元件
|
||||
│ ├── hooks/ # 自訂 React hooks
|
||||
│ ├── lib/ # 工具
|
||||
│ ├── types/ # TypeScript 定義
|
||||
│ └── config/ # 設定
|
||||
│
|
||||
├── backend/
|
||||
│ ├── routers/ # FastAPI 路由處理器
|
||||
│ ├── models.py # Pydantic 模型
|
||||
│ ├── main.py # FastAPI app 進入點
|
||||
│ ├── auth_system.py # 認證
|
||||
│ ├── database.py # 資料庫操作
|
||||
│ ├── services/ # 業務邏輯
|
||||
│ └── tests/ # pytest 測試
|
||||
│
|
||||
├── deploy/ # 部署設定
|
||||
├── docs/ # 文件
|
||||
└── scripts/ # 工具腳本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 程式碼模式
|
||||
|
||||
### API 回應格式(FastAPI)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Generic, TypeVar, Optional
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class ApiResponse(BaseModel, Generic[T]):
|
||||
success: bool
|
||||
data: Optional[T] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def ok(cls, data: T) -> "ApiResponse[T]":
|
||||
return cls(success=True, data=data)
|
||||
|
||||
@classmethod
|
||||
def fail(cls, error: str) -> "ApiResponse[T]":
|
||||
return cls(success=False, error=error)
|
||||
```
|
||||
|
||||
### 前端 API 呼叫(TypeScript)
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const response = await fetch(`/api${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `HTTP ${response.status}` }
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude AI 整合(結構化輸出)
|
||||
|
||||
```python
|
||||
from anthropic import Anthropic
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AnalysisResult(BaseModel):
|
||||
summary: str
|
||||
key_points: list[str]
|
||||
confidence: float
|
||||
|
||||
async def analyze_with_claude(content: str) -> AnalysisResult:
|
||||
client = Anthropic()
|
||||
|
||||
response = client.messages.create(
|
||||
model="claude-sonnet-4-5-20250514",
|
||||
max_tokens=1024,
|
||||
messages=[{"role": "user", "content": content}],
|
||||
tools=[{
|
||||
"name": "provide_analysis",
|
||||
"description": "Provide structured analysis",
|
||||
"input_schema": AnalysisResult.model_json_schema()
|
||||
}],
|
||||
tool_choice={"type": "tool", "name": "provide_analysis"}
|
||||
)
|
||||
|
||||
# 提取工具使用結果
|
||||
tool_use = next(
|
||||
block for block in response.content
|
||||
if block.type == "tool_use"
|
||||
)
|
||||
|
||||
return AnalysisResult(**tool_use.input)
|
||||
```
|
||||
|
||||
### 自訂 Hooks(React)
|
||||
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
interface UseApiState<T> {
|
||||
data: T | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function useApi<T>(
|
||||
fetchFn: () => Promise<ApiResponse<T>>
|
||||
) {
|
||||
const [state, setState] = useState<UseApiState<T>>({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||
|
||||
const result = await fetchFn()
|
||||
|
||||
if (result.success) {
|
||||
setState({ data: result.data!, loading: false, error: null })
|
||||
} else {
|
||||
setState({ data: null, loading: false, error: result.error! })
|
||||
}
|
||||
}, [fetchFn])
|
||||
|
||||
return { ...state, execute }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試要求
|
||||
|
||||
### 後端(pytest)
|
||||
|
||||
```bash
|
||||
# 執行所有測試
|
||||
poetry run pytest tests/
|
||||
|
||||
# 執行帶覆蓋率的測試
|
||||
poetry run pytest tests/ --cov=. --cov-report=html
|
||||
|
||||
# 執行特定測試檔案
|
||||
poetry run pytest tests/test_auth.py -v
|
||||
```
|
||||
|
||||
**測試結構:**
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from main import app
|
||||
|
||||
@pytest.fixture
|
||||
async def client():
|
||||
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check(client: AsyncClient):
|
||||
response = await client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "healthy"
|
||||
```
|
||||
|
||||
### 前端(React Testing Library)
|
||||
|
||||
```bash
|
||||
# 執行測試
|
||||
npm run test
|
||||
|
||||
# 執行帶覆蓋率的測試
|
||||
npm run test -- --coverage
|
||||
|
||||
# 執行 E2E 測試
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
**測試結構:**
|
||||
```typescript
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { WorkspacePanel } from './WorkspacePanel'
|
||||
|
||||
describe('WorkspacePanel', () => {
|
||||
it('renders workspace correctly', () => {
|
||||
render(<WorkspacePanel />)
|
||||
expect(screen.getByRole('main')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles session creation', async () => {
|
||||
render(<WorkspacePanel />)
|
||||
fireEvent.click(screen.getByText('New Session'))
|
||||
expect(await screen.findByText('Session created')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署工作流程
|
||||
|
||||
### 部署前檢查清單
|
||||
|
||||
- [ ] 本機所有測試通過
|
||||
- [ ] `npm run build` 成功(前端)
|
||||
- [ ] `poetry run pytest` 通過(後端)
|
||||
- [ ] 無寫死密鑰
|
||||
- [ ] 環境變數已記錄
|
||||
- [ ] 資料庫 migrations 準備就緒
|
||||
|
||||
### 部署指令
|
||||
|
||||
```bash
|
||||
# 建置和部署前端
|
||||
cd frontend && npm run build
|
||||
gcloud run deploy frontend --source .
|
||||
|
||||
# 建置和部署後端
|
||||
cd backend
|
||||
gcloud run deploy backend --source .
|
||||
```
|
||||
|
||||
### 環境變數
|
||||
|
||||
```bash
|
||||
# 前端(.env.local)
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||
|
||||
# 後端(.env)
|
||||
DATABASE_URL=postgresql://...
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
SUPABASE_URL=https://xxx.supabase.co
|
||||
SUPABASE_KEY=eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 關鍵規則
|
||||
|
||||
1. **無表情符號** 在程式碼、註解或文件中
|
||||
2. **不可變性** - 永遠不要突變物件或陣列
|
||||
3. **TDD** - 實作前先寫測試
|
||||
4. **80% 覆蓋率** 最低
|
||||
5. **多個小檔案** - 200-400 行典型,最多 800 行
|
||||
6. **無 console.log** 在生產程式碼中
|
||||
7. **適當錯誤處理** 使用 try/catch
|
||||
8. **輸入驗證** 使用 Pydantic/Zod
|
||||
|
||||
---
|
||||
|
||||
## 相關技能
|
||||
|
||||
- `coding-standards.md` - 一般程式碼最佳實務
|
||||
- `backend-patterns.md` - API 和資料庫模式
|
||||
- `frontend-patterns.md` - React 和 Next.js 模式
|
||||
- `tdd-workflow/` - 測試驅動開發方法論
|
||||
@@ -1,494 +0,0 @@
|
||||
---
|
||||
name: security-review
|
||||
description: Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
|
||||
---
|
||||
|
||||
# 安全性審查技能
|
||||
|
||||
此技能確保所有程式碼遵循安全性最佳實務並識別潛在漏洞。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 實作認證或授權
|
||||
- 處理使用者輸入或檔案上傳
|
||||
- 建立新的 API 端點
|
||||
- 處理密鑰或憑證
|
||||
- 實作支付功能
|
||||
- 儲存或傳輸敏感資料
|
||||
- 整合第三方 API
|
||||
|
||||
## 安全性檢查清單
|
||||
|
||||
### 1. 密鑰管理
|
||||
|
||||
#### ❌ 絕不這樣做
|
||||
```typescript
|
||||
const apiKey = "sk-proj-xxxxx" // 寫死的密鑰
|
||||
const dbPassword = "password123" // 在原始碼中
|
||||
```
|
||||
|
||||
#### ✅ 總是這樣做
|
||||
```typescript
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
|
||||
// 驗證密鑰存在
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 無寫死的 API 金鑰、Token 或密碼
|
||||
- [ ] 所有密鑰在環境變數中
|
||||
- [ ] `.env.local` 在 .gitignore 中
|
||||
- [ ] git 歷史中無密鑰
|
||||
- [ ] 生產密鑰在託管平台(Vercel、Railway)中
|
||||
|
||||
### 2. 輸入驗證
|
||||
|
||||
#### 總是驗證使用者輸入
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// 定義驗證 schema
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
// 處理前驗證
|
||||
export async function createUser(input: unknown) {
|
||||
try {
|
||||
const validated = CreateUserSchema.parse(input)
|
||||
return await db.users.create(validated)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { success: false, errors: error.errors }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 檔案上傳驗證
|
||||
```typescript
|
||||
function validateFileUpload(file: File) {
|
||||
// 大小檢查(最大 5MB)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large (max 5MB)')
|
||||
}
|
||||
|
||||
// 類型檢查
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type')
|
||||
}
|
||||
|
||||
// 副檔名檢查
|
||||
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
||||
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
|
||||
if (!extension || !allowedExtensions.includes(extension)) {
|
||||
throw new Error('Invalid file extension')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 所有使用者輸入以 schema 驗證
|
||||
- [ ] 檔案上傳受限(大小、類型、副檔名)
|
||||
- [ ] 查詢中不直接使用使用者輸入
|
||||
- [ ] 白名單驗證(非黑名單)
|
||||
- [ ] 錯誤訊息不洩露敏感資訊
|
||||
|
||||
### 3. SQL 注入預防
|
||||
|
||||
#### ❌ 絕不串接 SQL
|
||||
```typescript
|
||||
// 危險 - SQL 注入漏洞
|
||||
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
|
||||
await db.query(query)
|
||||
```
|
||||
|
||||
#### ✅ 總是使用參數化查詢
|
||||
```typescript
|
||||
// 安全 - 參數化查詢
|
||||
const { data } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('email', userEmail)
|
||||
|
||||
// 或使用原始 SQL
|
||||
await db.query(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
[userEmail]
|
||||
)
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 所有資料庫查詢使用參數化查詢
|
||||
- [ ] SQL 中無字串串接
|
||||
- [ ] ORM/查詢建構器正確使用
|
||||
- [ ] Supabase 查詢正確淨化
|
||||
|
||||
### 4. 認證與授權
|
||||
|
||||
#### JWT Token 處理
|
||||
```typescript
|
||||
// ❌ 錯誤:localStorage(易受 XSS 攻擊)
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
// ✅ 正確:httpOnly cookies
|
||||
res.setHeader('Set-Cookie',
|
||||
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
|
||||
```
|
||||
|
||||
#### 授權檢查
|
||||
```typescript
|
||||
export async function deleteUser(userId: string, requesterId: string) {
|
||||
// 總是先驗證授權
|
||||
const requester = await db.users.findUnique({
|
||||
where: { id: requesterId }
|
||||
})
|
||||
|
||||
if (requester.role !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// 繼續刪除
|
||||
await db.users.delete({ where: { id: userId } })
|
||||
}
|
||||
```
|
||||
|
||||
#### Row Level Security(Supabase)
|
||||
```sql
|
||||
-- 在所有表格上啟用 RLS
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 使用者只能查看自己的資料
|
||||
CREATE POLICY "Users view own data"
|
||||
ON users FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- 使用者只能更新自己的資料
|
||||
CREATE POLICY "Users update own data"
|
||||
ON users FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] Token 儲存在 httpOnly cookies(非 localStorage)
|
||||
- [ ] 敏感操作前有授權檢查
|
||||
- [ ] Supabase 已啟用 Row Level Security
|
||||
- [ ] 已實作基於角色的存取控制
|
||||
- [ ] 工作階段管理安全
|
||||
|
||||
### 5. XSS 預防
|
||||
|
||||
#### 淨化 HTML
|
||||
```typescript
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
|
||||
// 總是淨化使用者提供的 HTML
|
||||
function renderUserContent(html: string) {
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
|
||||
ALLOWED_ATTR: []
|
||||
})
|
||||
return <div dangerouslySetInnerHTML={{ __html: clean }} />
|
||||
}
|
||||
```
|
||||
|
||||
#### Content Security Policy
|
||||
```typescript
|
||||
// next.config.js
|
||||
const securityHeaders = [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self';
|
||||
connect-src 'self' https://api.example.com;
|
||||
`.replace(/\s{2,}/g, ' ').trim()
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 使用者提供的 HTML 已淨化
|
||||
- [ ] CSP headers 已設定
|
||||
- [ ] 無未驗證的動態內容渲染
|
||||
- [ ] 使用 React 內建 XSS 保護
|
||||
|
||||
### 6. CSRF 保護
|
||||
|
||||
#### CSRF Tokens
|
||||
```typescript
|
||||
import { csrf } from '@/lib/csrf'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const token = request.headers.get('X-CSRF-Token')
|
||||
|
||||
if (!csrf.verify(token)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// 處理請求
|
||||
}
|
||||
```
|
||||
|
||||
#### SameSite Cookies
|
||||
```typescript
|
||||
res.setHeader('Set-Cookie',
|
||||
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 狀態變更操作有 CSRF tokens
|
||||
- [ ] 所有 cookies 設定 SameSite=Strict
|
||||
- [ ] 已實作 Double-submit cookie 模式
|
||||
|
||||
### 7. 速率限制
|
||||
|
||||
#### API 速率限制
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 分鐘
|
||||
max: 100, // 每視窗 100 個請求
|
||||
message: 'Too many requests'
|
||||
})
|
||||
|
||||
// 套用到路由
|
||||
app.use('/api/', limiter)
|
||||
```
|
||||
|
||||
#### 昂貴操作
|
||||
```typescript
|
||||
// 搜尋的積極速率限制
|
||||
const searchLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分鐘
|
||||
max: 10, // 每分鐘 10 個請求
|
||||
message: 'Too many search requests'
|
||||
})
|
||||
|
||||
app.use('/api/search', searchLimiter)
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 所有 API 端點有速率限制
|
||||
- [ ] 昂貴操作有更嚴格限制
|
||||
- [ ] 基於 IP 的速率限制
|
||||
- [ ] 基於使用者的速率限制(已認證)
|
||||
|
||||
### 8. 敏感資料暴露
|
||||
|
||||
#### 日誌記錄
|
||||
```typescript
|
||||
// ❌ 錯誤:記錄敏感資料
|
||||
console.log('User login:', { email, password })
|
||||
console.log('Payment:', { cardNumber, cvv })
|
||||
|
||||
// ✅ 正確:遮蔽敏感資料
|
||||
console.log('User login:', { email, userId })
|
||||
console.log('Payment:', { last4: card.last4, userId })
|
||||
```
|
||||
|
||||
#### 錯誤訊息
|
||||
```typescript
|
||||
// ❌ 錯誤:暴露內部細節
|
||||
catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ 正確:通用錯誤訊息
|
||||
catch (error) {
|
||||
console.error('Internal error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 日誌中無密碼、token 或密鑰
|
||||
- [ ] 使用者收到通用錯誤訊息
|
||||
- [ ] 詳細錯誤只在伺服器日誌
|
||||
- [ ] 不向使用者暴露堆疊追蹤
|
||||
|
||||
### 9. 區塊鏈安全(Solana)
|
||||
|
||||
#### 錢包驗證
|
||||
```typescript
|
||||
import { verify } from '@solana/web3.js'
|
||||
|
||||
async function verifyWalletOwnership(
|
||||
publicKey: string,
|
||||
signature: string,
|
||||
message: string
|
||||
) {
|
||||
try {
|
||||
const isValid = verify(
|
||||
Buffer.from(message),
|
||||
Buffer.from(signature, 'base64'),
|
||||
Buffer.from(publicKey, 'base64')
|
||||
)
|
||||
return isValid
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 交易驗證
|
||||
```typescript
|
||||
async function verifyTransaction(transaction: Transaction) {
|
||||
// 驗證收款人
|
||||
if (transaction.to !== expectedRecipient) {
|
||||
throw new Error('Invalid recipient')
|
||||
}
|
||||
|
||||
// 驗證金額
|
||||
if (transaction.amount > maxAmount) {
|
||||
throw new Error('Amount exceeds limit')
|
||||
}
|
||||
|
||||
// 驗證使用者有足夠餘額
|
||||
const balance = await getBalance(transaction.from)
|
||||
if (balance < transaction.amount) {
|
||||
throw new Error('Insufficient balance')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 錢包簽章已驗證
|
||||
- [ ] 交易詳情已驗證
|
||||
- [ ] 交易前有餘額檢查
|
||||
- [ ] 無盲目交易簽署
|
||||
|
||||
### 10. 依賴安全
|
||||
|
||||
#### 定期更新
|
||||
```bash
|
||||
# 檢查漏洞
|
||||
npm audit
|
||||
|
||||
# 自動修復可修復的問題
|
||||
npm audit fix
|
||||
|
||||
# 更新依賴
|
||||
npm update
|
||||
|
||||
# 檢查過時套件
|
||||
npm outdated
|
||||
```
|
||||
|
||||
#### Lock 檔案
|
||||
```bash
|
||||
# 總是 commit lock 檔案
|
||||
git add package-lock.json
|
||||
|
||||
# 在 CI/CD 中使用以獲得可重現的建置
|
||||
npm ci # 而非 npm install
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
- [ ] 依賴保持最新
|
||||
- [ ] 無已知漏洞(npm audit 乾淨)
|
||||
- [ ] Lock 檔案已 commit
|
||||
- [ ] GitHub 上已啟用 Dependabot
|
||||
- [ ] 定期安全更新
|
||||
|
||||
## 安全測試
|
||||
|
||||
### 自動化安全測試
|
||||
```typescript
|
||||
// 測試認證
|
||||
test('requires authentication', async () => {
|
||||
const response = await fetch('/api/protected')
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
// 測試授權
|
||||
test('requires admin role', async () => {
|
||||
const response = await fetch('/api/admin', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
})
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
// 測試輸入驗證
|
||||
test('rejects invalid input', async () => {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: 'not-an-email' })
|
||||
})
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
// 測試速率限制
|
||||
test('enforces rate limits', async () => {
|
||||
const requests = Array(101).fill(null).map(() =>
|
||||
fetch('/api/endpoint')
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
const tooManyRequests = responses.filter(r => r.status === 429)
|
||||
|
||||
expect(tooManyRequests.length).toBeGreaterThan(0)
|
||||
})
|
||||
```
|
||||
|
||||
## 部署前安全檢查清單
|
||||
|
||||
任何生產部署前:
|
||||
|
||||
- [ ] **密鑰**:無寫死密鑰,全在環境變數中
|
||||
- [ ] **輸入驗證**:所有使用者輸入已驗證
|
||||
- [ ] **SQL 注入**:所有查詢已參數化
|
||||
- [ ] **XSS**:使用者內容已淨化
|
||||
- [ ] **CSRF**:保護已啟用
|
||||
- [ ] **認證**:正確的 token 處理
|
||||
- [ ] **授權**:角色檢查已就位
|
||||
- [ ] **速率限制**:所有端點已啟用
|
||||
- [ ] **HTTPS**:生產環境強制使用
|
||||
- [ ] **安全標頭**:CSP、X-Frame-Options 已設定
|
||||
- [ ] **錯誤處理**:錯誤中無敏感資料
|
||||
- [ ] **日誌記錄**:無敏感資料被記錄
|
||||
- [ ] **依賴**:最新,無漏洞
|
||||
- [ ] **Row Level Security**:Supabase 已啟用
|
||||
- [ ] **CORS**:正確設定
|
||||
- [ ] **檔案上傳**:已驗證(大小、類型)
|
||||
- [ ] **錢包簽章**:已驗證(如果是區塊鏈)
|
||||
|
||||
## 資源
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Next.js Security](https://nextjs.org/docs/security)
|
||||
- [Supabase Security](https://supabase.com/docs/guides/auth)
|
||||
- [Web Security Academy](https://portswigger.net/web-security)
|
||||
|
||||
---
|
||||
|
||||
**記住**:安全性不是可選的。一個漏洞可能危及整個平台。有疑慮時,選擇謹慎的做法。
|
||||
@@ -1,361 +0,0 @@
|
||||
| name | description |
|
||||
|------|-------------|
|
||||
| cloud-infrastructure-security | Use this skill when deploying to cloud platforms, configuring infrastructure, managing IAM policies, setting up logging/monitoring, or implementing CI/CD pipelines. Provides cloud security checklist aligned with best practices. |
|
||||
|
||||
# 雲端與基礎設施安全技能
|
||||
|
||||
此技能確保雲端基礎設施、CI/CD 管線和部署設定遵循安全最佳實務並符合業界標準。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 部署應用程式到雲端平台(AWS、Vercel、Railway、Cloudflare)
|
||||
- 設定 IAM 角色和權限
|
||||
- 設置 CI/CD 管線
|
||||
- 實作基礎設施即程式碼(Terraform、CloudFormation)
|
||||
- 設定日誌和監控
|
||||
- 在雲端環境管理密鑰
|
||||
- 設置 CDN 和邊緣安全
|
||||
- 實作災難復原和備份策略
|
||||
|
||||
## 雲端安全檢查清單
|
||||
|
||||
### 1. IAM 與存取控制
|
||||
|
||||
#### 最小權限原則
|
||||
|
||||
```yaml
|
||||
# ✅ 正確:最小權限
|
||||
iam_role:
|
||||
permissions:
|
||||
- s3:GetObject # 只有讀取存取
|
||||
- s3:ListBucket
|
||||
resources:
|
||||
- arn:aws:s3:::my-bucket/* # 只有特定 bucket
|
||||
|
||||
# ❌ 錯誤:過於廣泛的權限
|
||||
iam_role:
|
||||
permissions:
|
||||
- s3:* # 所有 S3 動作
|
||||
resources:
|
||||
- "*" # 所有資源
|
||||
```
|
||||
|
||||
#### 多因素認證(MFA)
|
||||
|
||||
```bash
|
||||
# 總是為 root/admin 帳戶啟用 MFA
|
||||
aws iam enable-mfa-device \
|
||||
--user-name admin \
|
||||
--serial-number arn:aws:iam::123456789:mfa/admin \
|
||||
--authentication-code1 123456 \
|
||||
--authentication-code2 789012
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 生產環境不使用 root 帳戶
|
||||
- [ ] 所有特權帳戶啟用 MFA
|
||||
- [ ] 服務帳戶使用角色,非長期憑證
|
||||
- [ ] IAM 政策遵循最小權限
|
||||
- [ ] 定期進行存取審查
|
||||
- [ ] 未使用憑證已輪換或移除
|
||||
|
||||
### 2. 密鑰管理
|
||||
|
||||
#### 雲端密鑰管理器
|
||||
|
||||
```typescript
|
||||
// ✅ 正確:使用雲端密鑰管理器
|
||||
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
|
||||
|
||||
const client = new SecretsManager({ region: 'us-east-1' });
|
||||
const secret = await client.getSecretValue({ SecretId: 'prod/api-key' });
|
||||
const apiKey = JSON.parse(secret.SecretString).key;
|
||||
|
||||
// ❌ 錯誤:寫死或只在環境變數
|
||||
const apiKey = process.env.API_KEY; // 未輪換、未稽核
|
||||
```
|
||||
|
||||
#### 密鑰輪換
|
||||
|
||||
```bash
|
||||
# 為資料庫憑證設定自動輪換
|
||||
aws secretsmanager rotate-secret \
|
||||
--secret-id prod/db-password \
|
||||
--rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \
|
||||
--rotation-rules AutomaticallyAfterDays=30
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 所有密鑰儲存在雲端密鑰管理器(AWS Secrets Manager、Vercel Secrets)
|
||||
- [ ] 資料庫憑證啟用自動輪換
|
||||
- [ ] API 金鑰至少每季輪換
|
||||
- [ ] 程式碼、日誌或錯誤訊息中無密鑰
|
||||
- [ ] 密鑰存取啟用稽核日誌
|
||||
|
||||
### 3. 網路安全
|
||||
|
||||
#### VPC 和防火牆設定
|
||||
|
||||
```terraform
|
||||
# ✅ 正確:限制的安全群組
|
||||
resource "aws_security_group" "app" {
|
||||
name = "app-sg"
|
||||
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"] # 只有內部 VPC
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # 只有 HTTPS 輸出
|
||||
}
|
||||
}
|
||||
|
||||
# ❌ 錯誤:對網際網路開放
|
||||
resource "aws_security_group" "bad" {
|
||||
ingress {
|
||||
from_port = 0
|
||||
to_port = 65535
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # 所有埠、所有 IP!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 資料庫不可公開存取
|
||||
- [ ] SSH/RDP 埠限制為 VPN/堡壘機
|
||||
- [ ] 安全群組遵循最小權限
|
||||
- [ ] 網路 ACL 已設定
|
||||
- [ ] VPC 流量日誌已啟用
|
||||
|
||||
### 4. 日誌與監控
|
||||
|
||||
#### CloudWatch/日誌設定
|
||||
|
||||
```typescript
|
||||
// ✅ 正確:全面日誌記錄
|
||||
import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs';
|
||||
|
||||
const logSecurityEvent = async (event: SecurityEvent) => {
|
||||
await cloudwatch.putLogEvents({
|
||||
logGroupName: '/aws/security/events',
|
||||
logStreamName: 'authentication',
|
||||
logEvents: [{
|
||||
timestamp: Date.now(),
|
||||
message: JSON.stringify({
|
||||
type: event.type,
|
||||
userId: event.userId,
|
||||
ip: event.ip,
|
||||
result: event.result,
|
||||
// 永遠不要記錄敏感資料
|
||||
})
|
||||
}]
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 所有服務啟用 CloudWatch/日誌記錄
|
||||
- [ ] 失敗的認證嘗試被記錄
|
||||
- [ ] 管理員動作被稽核
|
||||
- [ ] 日誌保留已設定(合規需 90+ 天)
|
||||
- [ ] 可疑活動設定警報
|
||||
- [ ] 日誌集中化且防篡改
|
||||
|
||||
### 5. CI/CD 管線安全
|
||||
|
||||
#### 安全管線設定
|
||||
|
||||
```yaml
|
||||
# ✅ 正確:安全的 GitHub Actions 工作流程
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # 最小權限
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# 掃描密鑰
|
||||
- name: Secret scanning
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
|
||||
# 依賴稽核
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level=high
|
||||
|
||||
# 使用 OIDC,非長期 tokens
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
|
||||
aws-region: us-east-1
|
||||
```
|
||||
|
||||
#### 供應鏈安全
|
||||
|
||||
```json
|
||||
// package.json - 使用 lock 檔案和完整性檢查
|
||||
{
|
||||
"scripts": {
|
||||
"install": "npm ci", // 使用 ci 以獲得可重現建置
|
||||
"audit": "npm audit --audit-level=moderate",
|
||||
"check": "npm outdated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 使用 OIDC 而非長期憑證
|
||||
- [ ] 管線中的密鑰掃描
|
||||
- [ ] 依賴漏洞掃描
|
||||
- [ ] 容器映像掃描(如適用)
|
||||
- [ ] 強制執行分支保護規則
|
||||
- [ ] 合併前需要程式碼審查
|
||||
- [ ] 強制執行簽署 commits
|
||||
|
||||
### 6. Cloudflare 與 CDN 安全
|
||||
|
||||
#### Cloudflare 安全設定
|
||||
|
||||
```typescript
|
||||
// ✅ 正確:帶安全標頭的 Cloudflare Workers
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const response = await fetch(request);
|
||||
|
||||
// 新增安全標頭
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('X-Frame-Options', 'DENY');
|
||||
headers.set('X-Content-Type-Options', 'nosniff');
|
||||
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
headers.set('Permissions-Policy', 'geolocation=(), microphone=()');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### WAF 規則
|
||||
|
||||
```bash
|
||||
# 啟用 Cloudflare WAF 管理規則
|
||||
# - OWASP 核心規則集
|
||||
# - Cloudflare 管理規則集
|
||||
# - 速率限制規則
|
||||
# - Bot 保護
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] WAF 啟用 OWASP 規則
|
||||
- [ ] 速率限制已設定
|
||||
- [ ] Bot 保護啟用
|
||||
- [ ] DDoS 保護啟用
|
||||
- [ ] 安全標頭已設定
|
||||
- [ ] SSL/TLS 嚴格模式啟用
|
||||
|
||||
### 7. 備份與災難復原
|
||||
|
||||
#### 自動備份
|
||||
|
||||
```terraform
|
||||
# ✅ 正確:自動 RDS 備份
|
||||
resource "aws_db_instance" "main" {
|
||||
allocated_storage = 20
|
||||
engine = "postgres"
|
||||
|
||||
backup_retention_period = 30 # 30 天保留
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "mon:04:00-mon:05:00"
|
||||
|
||||
enabled_cloudwatch_logs_exports = ["postgresql"]
|
||||
|
||||
deletion_protection = true # 防止意外刪除
|
||||
}
|
||||
```
|
||||
|
||||
#### 驗證步驟
|
||||
|
||||
- [ ] 已設定自動每日備份
|
||||
- [ ] 備份保留符合合規要求
|
||||
- [ ] 已啟用時間點復原
|
||||
- [ ] 每季執行備份測試
|
||||
- [ ] 災難復原計畫已記錄
|
||||
- [ ] RPO 和 RTO 已定義並測試
|
||||
|
||||
## 部署前雲端安全檢查清單
|
||||
|
||||
任何生產雲端部署前:
|
||||
|
||||
- [ ] **IAM**:不使用 root 帳戶、啟用 MFA、最小權限政策
|
||||
- [ ] **密鑰**:所有密鑰在雲端密鑰管理器並有輪換
|
||||
- [ ] **網路**:安全群組受限、無公開資料庫
|
||||
- [ ] **日誌**:CloudWatch/日誌啟用並有保留
|
||||
- [ ] **監控**:異常設定警報
|
||||
- [ ] **CI/CD**:OIDC 認證、密鑰掃描、依賴稽核
|
||||
- [ ] **CDN/WAF**:Cloudflare WAF 啟用 OWASP 規則
|
||||
- [ ] **加密**:資料靜態和傳輸中加密
|
||||
- [ ] **備份**:自動備份並測試復原
|
||||
- [ ] **合規**:符合 GDPR/HIPAA 要求(如適用)
|
||||
- [ ] **文件**:基礎設施已記錄、建立操作手冊
|
||||
- [ ] **事件回應**:安全事件計畫就位
|
||||
|
||||
## 常見雲端安全錯誤設定
|
||||
|
||||
### S3 Bucket 暴露
|
||||
|
||||
```bash
|
||||
# ❌ 錯誤:公開 bucket
|
||||
aws s3api put-bucket-acl --bucket my-bucket --acl public-read
|
||||
|
||||
# ✅ 正確:私有 bucket 並有特定存取
|
||||
aws s3api put-bucket-acl --bucket my-bucket --acl private
|
||||
aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
|
||||
```
|
||||
|
||||
### RDS 公開存取
|
||||
|
||||
```terraform
|
||||
# ❌ 錯誤
|
||||
resource "aws_db_instance" "bad" {
|
||||
publicly_accessible = true # 絕不這樣做!
|
||||
}
|
||||
|
||||
# ✅ 正確
|
||||
resource "aws_db_instance" "good" {
|
||||
publicly_accessible = false
|
||||
vpc_security_group_ids = [aws_security_group.db.id]
|
||||
}
|
||||
```
|
||||
|
||||
## 資源
|
||||
|
||||
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
|
||||
- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services)
|
||||
- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/)
|
||||
- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/)
|
||||
- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/)
|
||||
|
||||
**記住**:雲端錯誤設定是資料外洩的主要原因。單一暴露的 S3 bucket 或過於寬鬆的 IAM 政策可能危及你的整個基礎設施。總是遵循最小權限原則和深度防禦。
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
name: strategic-compact
|
||||
description: Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction.
|
||||
---
|
||||
|
||||
# 策略性壓縮技能
|
||||
|
||||
在工作流程的策略點建議手動 `/compact`,而非依賴任意的自動壓縮。
|
||||
|
||||
## 為什麼需要策略性壓縮?
|
||||
|
||||
自動壓縮在任意點觸發:
|
||||
- 經常在任務中途,丟失重要上下文
|
||||
- 不知道邏輯任務邊界
|
||||
- 可能中斷複雜的多步驟操作
|
||||
|
||||
邏輯邊界的策略性壓縮:
|
||||
- **探索後、執行前** - 壓縮研究上下文,保留實作計畫
|
||||
- **完成里程碑後** - 為下一階段重新開始
|
||||
- **主要上下文轉換前** - 在不同任務前清除探索上下文
|
||||
|
||||
## 運作方式
|
||||
|
||||
`suggest-compact.sh` 腳本在 PreToolUse(Edit/Write)執行並:
|
||||
|
||||
1. **追蹤工具呼叫** - 計算工作階段中的工具呼叫次數
|
||||
2. **門檻偵測** - 在可設定門檻建議(預設:50 次呼叫)
|
||||
3. **定期提醒** - 門檻後每 25 次呼叫提醒一次
|
||||
|
||||
## Hook 設定
|
||||
|
||||
新增到你的 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "tool == \"Edit\" || tool == \"Write\"",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/strategic-compact/suggest-compact.sh"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 設定
|
||||
|
||||
環境變數:
|
||||
- `COMPACT_THRESHOLD` - 第一次建議前的工具呼叫次數(預設:50)
|
||||
|
||||
## 最佳實務
|
||||
|
||||
1. **規劃後壓縮** - 計畫確定後,壓縮以重新開始
|
||||
2. **除錯後壓縮** - 繼續前清除錯誤解決上下文
|
||||
3. **不要在實作中途壓縮** - 為相關變更保留上下文
|
||||
4. **閱讀建議** - Hook 告訴你*何時*,你決定*是否*
|
||||
|
||||
## 相關
|
||||
|
||||
- [Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Token 優化章節
|
||||
- 記憶持久性 hooks - 用於壓縮後存活的狀態
|
||||
@@ -1,409 +0,0 @@
|
||||
---
|
||||
name: tdd-workflow
|
||||
description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
|
||||
---
|
||||
|
||||
# 測試驅動開發工作流程
|
||||
|
||||
此技能確保所有程式碼開發遵循 TDD 原則,並具有完整的測試覆蓋率。
|
||||
|
||||
## 何時啟用
|
||||
|
||||
- 撰寫新功能或功能性程式碼
|
||||
- 修復 Bug 或問題
|
||||
- 重構現有程式碼
|
||||
- 新增 API 端點
|
||||
- 建立新元件
|
||||
|
||||
## 核心原則
|
||||
|
||||
### 1. 測試先於程式碼
|
||||
總是先寫測試,然後實作程式碼使測試通過。
|
||||
|
||||
### 2. 覆蓋率要求
|
||||
- 最低 80% 覆蓋率(單元 + 整合 + E2E)
|
||||
- 涵蓋所有邊界案例
|
||||
- 測試錯誤情境
|
||||
- 驗證邊界條件
|
||||
|
||||
### 3. 測試類型
|
||||
|
||||
#### 單元測試
|
||||
- 個別函式和工具
|
||||
- 元件邏輯
|
||||
- 純函式
|
||||
- 輔助函式和工具
|
||||
|
||||
#### 整合測試
|
||||
- API 端點
|
||||
- 資料庫操作
|
||||
- 服務互動
|
||||
- 外部 API 呼叫
|
||||
|
||||
#### E2E 測試(Playwright)
|
||||
- 關鍵使用者流程
|
||||
- 完整工作流程
|
||||
- 瀏覽器自動化
|
||||
- UI 互動
|
||||
|
||||
## TDD 工作流程步驟
|
||||
|
||||
### 步驟 1:撰寫使用者旅程
|
||||
```
|
||||
身為 [角色],我想要 [動作],以便 [好處]
|
||||
|
||||
範例:
|
||||
身為使用者,我想要語意搜尋市場,
|
||||
以便即使沒有精確關鍵字也能找到相關市場。
|
||||
```
|
||||
|
||||
### 步驟 2:產生測試案例
|
||||
為每個使用者旅程建立完整的測試案例:
|
||||
|
||||
```typescript
|
||||
describe('Semantic Search', () => {
|
||||
it('returns relevant markets for query', async () => {
|
||||
// 測試實作
|
||||
})
|
||||
|
||||
it('handles empty query gracefully', async () => {
|
||||
// 測試邊界案例
|
||||
})
|
||||
|
||||
it('falls back to substring search when Redis unavailable', async () => {
|
||||
// 測試回退行為
|
||||
})
|
||||
|
||||
it('sorts results by similarity score', async () => {
|
||||
// 測試排序邏輯
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 步驟 3:執行測試(應該失敗)
|
||||
```bash
|
||||
npm test
|
||||
# 測試應該失敗 - 我們還沒實作
|
||||
```
|
||||
|
||||
### 步驟 4:實作程式碼
|
||||
撰寫最少的程式碼使測試通過:
|
||||
|
||||
```typescript
|
||||
// 由測試引導的實作
|
||||
export async function searchMarkets(query: string) {
|
||||
// 實作在此
|
||||
}
|
||||
```
|
||||
|
||||
### 步驟 5:再次執行測試
|
||||
```bash
|
||||
npm test
|
||||
# 測試現在應該通過
|
||||
```
|
||||
|
||||
### 步驟 6:重構
|
||||
在保持測試通過的同時改善程式碼品質:
|
||||
- 移除重複
|
||||
- 改善命名
|
||||
- 優化效能
|
||||
- 增強可讀性
|
||||
|
||||
### 步驟 7:驗證覆蓋率
|
||||
```bash
|
||||
npm run test:coverage
|
||||
# 驗證達到 80%+ 覆蓋率
|
||||
```
|
||||
|
||||
## 測試模式
|
||||
|
||||
### 單元測試模式(Jest/Vitest)
|
||||
```typescript
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { Button } from './Button'
|
||||
|
||||
describe('Button Component', () => {
|
||||
it('renders with correct text', () => {
|
||||
render(<Button>Click me</Button>)
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onClick when clicked', () => {
|
||||
const handleClick = jest.fn()
|
||||
render(<Button onClick={handleClick}>Click</Button>)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Button disabled>Click</Button>)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### API 整合測試模式
|
||||
```typescript
|
||||
import { NextRequest } from 'next/server'
|
||||
import { GET } from './route'
|
||||
|
||||
describe('GET /api/markets', () => {
|
||||
it('returns markets successfully', async () => {
|
||||
const request = new NextRequest('http://localhost/api/markets')
|
||||
const response = await GET(request)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(Array.isArray(data.data)).toBe(true)
|
||||
})
|
||||
|
||||
it('validates query parameters', async () => {
|
||||
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
|
||||
const response = await GET(request)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('handles database errors gracefully', async () => {
|
||||
// Mock 資料庫失敗
|
||||
const request = new NextRequest('http://localhost/api/markets')
|
||||
// 測試錯誤處理
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E 測試模式(Playwright)
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('user can search and filter markets', async ({ page }) => {
|
||||
// 導航到市場頁面
|
||||
await page.goto('/')
|
||||
await page.click('a[href="/markets"]')
|
||||
|
||||
// 驗證頁面載入
|
||||
await expect(page.locator('h1')).toContainText('Markets')
|
||||
|
||||
// 搜尋市場
|
||||
await page.fill('input[placeholder="Search markets"]', 'election')
|
||||
|
||||
// 等待 debounce 和結果
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// 驗證搜尋結果顯示
|
||||
const results = page.locator('[data-testid="market-card"]')
|
||||
await expect(results).toHaveCount(5, { timeout: 5000 })
|
||||
|
||||
// 驗證結果包含搜尋詞
|
||||
const firstResult = results.first()
|
||||
await expect(firstResult).toContainText('election', { ignoreCase: true })
|
||||
|
||||
// 依狀態篩選
|
||||
await page.click('button:has-text("Active")')
|
||||
|
||||
// 驗證篩選結果
|
||||
await expect(results).toHaveCount(3)
|
||||
})
|
||||
|
||||
test('user can create a new market', async ({ page }) => {
|
||||
// 先登入
|
||||
await page.goto('/creator-dashboard')
|
||||
|
||||
// 填寫市場建立表單
|
||||
await page.fill('input[name="name"]', 'Test Market')
|
||||
await page.fill('textarea[name="description"]', 'Test description')
|
||||
await page.fill('input[name="endDate"]', '2025-12-31')
|
||||
|
||||
// 提交表單
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// 驗證成功訊息
|
||||
await expect(page.locator('text=Market created successfully')).toBeVisible()
|
||||
|
||||
// 驗證重導向到市場頁面
|
||||
await expect(page).toHaveURL(/\/markets\/test-market/)
|
||||
})
|
||||
```
|
||||
|
||||
## 測試檔案組織
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.test.tsx # 單元測試
|
||||
│ │ └── Button.stories.tsx # Storybook
|
||||
│ └── MarketCard/
|
||||
│ ├── MarketCard.tsx
|
||||
│ └── MarketCard.test.tsx
|
||||
├── app/
|
||||
│ └── api/
|
||||
│ └── markets/
|
||||
│ ├── route.ts
|
||||
│ └── route.test.ts # 整合測試
|
||||
└── e2e/
|
||||
├── markets.spec.ts # E2E 測試
|
||||
├── trading.spec.ts
|
||||
└── auth.spec.ts
|
||||
```
|
||||
|
||||
## Mock 外部服務
|
||||
|
||||
### Supabase Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/supabase', () => ({
|
||||
supabase: {
|
||||
from: jest.fn(() => ({
|
||||
select: jest.fn(() => ({
|
||||
eq: jest.fn(() => Promise.resolve({
|
||||
data: [{ id: 1, name: 'Test Market' }],
|
||||
error: null
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
### Redis Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/redis', () => ({
|
||||
searchMarketsByVector: jest.fn(() => Promise.resolve([
|
||||
{ slug: 'test-market', similarity_score: 0.95 }
|
||||
])),
|
||||
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
|
||||
}))
|
||||
```
|
||||
|
||||
### OpenAI Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/openai', () => ({
|
||||
generateEmbedding: jest.fn(() => Promise.resolve(
|
||||
new Array(1536).fill(0.1) // Mock 1536 維嵌入向量
|
||||
))
|
||||
}))
|
||||
```
|
||||
|
||||
## 測試覆蓋率驗證
|
||||
|
||||
### 執行覆蓋率報告
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### 覆蓋率門檻
|
||||
```json
|
||||
{
|
||||
"jest": {
|
||||
"coverageThresholds": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常見測試錯誤避免
|
||||
|
||||
### ❌ 錯誤:測試實作細節
|
||||
```typescript
|
||||
// 不要測試內部狀態
|
||||
expect(component.state.count).toBe(5)
|
||||
```
|
||||
|
||||
### ✅ 正確:測試使用者可見行為
|
||||
```typescript
|
||||
// 測試使用者看到的內容
|
||||
expect(screen.getByText('Count: 5')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### ❌ 錯誤:脆弱的選擇器
|
||||
```typescript
|
||||
// 容易壞掉
|
||||
await page.click('.css-class-xyz')
|
||||
```
|
||||
|
||||
### ✅ 正確:語意選擇器
|
||||
```typescript
|
||||
// 對變更有彈性
|
||||
await page.click('button:has-text("Submit")')
|
||||
await page.click('[data-testid="submit-button"]')
|
||||
```
|
||||
|
||||
### ❌ 錯誤:無測試隔離
|
||||
```typescript
|
||||
// 測試互相依賴
|
||||
test('creates user', () => { /* ... */ })
|
||||
test('updates same user', () => { /* 依賴前一個測試 */ })
|
||||
```
|
||||
|
||||
### ✅ 正確:獨立測試
|
||||
```typescript
|
||||
// 每個測試設置自己的資料
|
||||
test('creates user', () => {
|
||||
const user = createTestUser()
|
||||
// 測試邏輯
|
||||
})
|
||||
|
||||
test('updates user', () => {
|
||||
const user = createTestUser()
|
||||
// 更新邏輯
|
||||
})
|
||||
```
|
||||
|
||||
## 持續測試
|
||||
|
||||
### 開發期間的 Watch 模式
|
||||
```bash
|
||||
npm test -- --watch
|
||||
# 檔案變更時自動執行測試
|
||||
```
|
||||
|
||||
### Pre-Commit Hook
|
||||
```bash
|
||||
# 每次 commit 前執行
|
||||
npm test && npm run lint
|
||||
```
|
||||
|
||||
### CI/CD 整合
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Run Tests
|
||||
run: npm test -- --coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## 最佳實務
|
||||
|
||||
1. **先寫測試** - 總是 TDD
|
||||
2. **一個測試一個斷言** - 專注單一行為
|
||||
3. **描述性測試名稱** - 解釋測試內容
|
||||
4. **Arrange-Act-Assert** - 清晰的測試結構
|
||||
5. **Mock 外部依賴** - 隔離單元測試
|
||||
6. **測試邊界案例** - Null、undefined、空值、大值
|
||||
7. **測試錯誤路徑** - 不只是快樂路徑
|
||||
8. **保持測試快速** - 單元測試每個 < 50ms
|
||||
9. **測試後清理** - 無副作用
|
||||
10. **檢視覆蓋率報告** - 識別缺口
|
||||
|
||||
## 成功指標
|
||||
|
||||
- 達到 80%+ 程式碼覆蓋率
|
||||
- 所有測試通過(綠色)
|
||||
- 無跳過或停用的測試
|
||||
- 快速測試執行(單元測試 < 30s)
|
||||
- E2E 測試涵蓋關鍵使用者流程
|
||||
- 測試在生產前捕捉 Bug
|
||||
|
||||
---
|
||||
|
||||
**記住**:測試不是可選的。它們是實現自信重構、快速開發和生產可靠性的安全網。
|
||||
@@ -1,120 +0,0 @@
|
||||
# 驗證循環技能
|
||||
|
||||
Claude Code 工作階段的完整驗證系統。
|
||||
|
||||
## 何時使用
|
||||
|
||||
在以下情況呼叫此技能:
|
||||
- 完成功能或重大程式碼變更後
|
||||
- 建立 PR 前
|
||||
- 想確保品質門檻通過時
|
||||
- 重構後
|
||||
|
||||
## 驗證階段
|
||||
|
||||
### 階段 1:建置驗證
|
||||
```bash
|
||||
# 檢查專案是否建置
|
||||
npm run build 2>&1 | tail -20
|
||||
# 或
|
||||
pnpm build 2>&1 | tail -20
|
||||
```
|
||||
|
||||
如果建置失敗,停止並在繼續前修復。
|
||||
|
||||
### 階段 2:型別檢查
|
||||
```bash
|
||||
# TypeScript 專案
|
||||
npx tsc --noEmit 2>&1 | head -30
|
||||
|
||||
# Python 專案
|
||||
pyright . 2>&1 | head -30
|
||||
```
|
||||
|
||||
報告所有型別錯誤。繼續前修復關鍵錯誤。
|
||||
|
||||
### 階段 3:Lint 檢查
|
||||
```bash
|
||||
# JavaScript/TypeScript
|
||||
npm run lint 2>&1 | head -30
|
||||
|
||||
# Python
|
||||
ruff check . 2>&1 | head -30
|
||||
```
|
||||
|
||||
### 階段 4:測試套件
|
||||
```bash
|
||||
# 執行帶覆蓋率的測試
|
||||
npm run test -- --coverage 2>&1 | tail -50
|
||||
|
||||
# 檢查覆蓋率門檻
|
||||
# 目標:最低 80%
|
||||
```
|
||||
|
||||
報告:
|
||||
- 總測試數:X
|
||||
- 通過:X
|
||||
- 失敗:X
|
||||
- 覆蓋率:X%
|
||||
|
||||
### 階段 5:安全掃描
|
||||
```bash
|
||||
# 檢查密鑰
|
||||
grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||
grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||
|
||||
# 檢查 console.log
|
||||
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
|
||||
```
|
||||
|
||||
### 階段 6:差異審查
|
||||
```bash
|
||||
# 顯示變更內容
|
||||
git diff --stat
|
||||
git diff HEAD~1 --name-only
|
||||
```
|
||||
|
||||
審查每個變更的檔案:
|
||||
- 非預期變更
|
||||
- 缺少錯誤處理
|
||||
- 潛在邊界案例
|
||||
|
||||
## 輸出格式
|
||||
|
||||
執行所有階段後,產生驗證報告:
|
||||
|
||||
```
|
||||
驗證報告
|
||||
==================
|
||||
|
||||
建置: [PASS/FAIL]
|
||||
型別: [PASS/FAIL](X 個錯誤)
|
||||
Lint: [PASS/FAIL](X 個警告)
|
||||
測試: [PASS/FAIL](X/Y 通過,Z% 覆蓋率)
|
||||
安全性: [PASS/FAIL](X 個問題)
|
||||
差異: [X 個檔案變更]
|
||||
|
||||
整體: [READY/NOT READY] for PR
|
||||
|
||||
待修復問題:
|
||||
1. ...
|
||||
2. ...
|
||||
```
|
||||
|
||||
## 持續模式
|
||||
|
||||
對於長時間工作階段,每 15 分鐘或重大變更後執行驗證:
|
||||
|
||||
```markdown
|
||||
設定心理檢查點:
|
||||
- 完成每個函式後
|
||||
- 完成元件後
|
||||
- 移至下一個任務前
|
||||
|
||||
執行:/verify
|
||||
```
|
||||
|
||||
## 與 Hooks 整合
|
||||
|
||||
此技能補充 PostToolUse hooks 但提供更深入的驗證。
|
||||
Hooks 立即捕捉問題;此技能提供全面審查。
|
||||
Reference in New Issue
Block a user