docs: add Traditional Chinese translation

Complete Traditional Chinese (zh-TW) translation of documentation
This commit is contained in:
Dave Lin
2026-01-29 15:06:29 +08:00
committed by GitHub
parent fbe2e56677
commit c3430bdc8a
59 changed files with 12017 additions and 0 deletions

View File

@@ -0,0 +1,587 @@
---
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 })
}
}
```
**記住**:後端模式能實現可擴展、可維護的伺服器端應用程式。選擇符合你複雜度等級的模式。

View File

@@ -0,0 +1,429 @@
---
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 擅長分析工作負載。為你的查詢模式設計表格,批量插入,並利用物化視圖進行即時聚合。

View File

@@ -0,0 +1,520 @@
---
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)
```
**記住**:程式碼品質是不可協商的。清晰、可維護的程式碼能實現快速開發和自信的重構。

View File

@@ -0,0 +1,257 @@
---
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/PostToolUse100% 可靠) |
| 分析 | 主要上下文 | 背景 agentHaiku |
| 粒度 | 完整技能 | 原子「本能」 |
| 信心 | 無 | 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 你的模式。*

View File

@@ -0,0 +1,110 @@
---
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 hooks100% 可靠) |
| 分析 | 主要上下文 | 背景 agentHaiku |
| 粒度 | 完整技能 | 原子「本能」 |
| 信心 | 無 | 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` 完整規格。

View File

@@ -0,0 +1,227 @@
---
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-51=差5=優秀)
理由:[解釋]
```
### 3. 人工評分器
標記為手動審查:
```markdown
[HUMAN REVIEW REQUIRED]
變更:變更內容的描述
理由:為何需要人工審查
風險等級LOW/MEDIUM/HIGH
```
## 指標
### pass@k
「k 次嘗試中至少一次成功」
- pass@1:第一次嘗試成功率
- pass@33 次嘗試內成功
- 典型目標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
## EVALadd-authentication
### 階段 1定義10 分鐘)
能力 Evals
- [ ] 使用者可以用電子郵件/密碼註冊
- [ ] 使用者可以用有效憑證登入
- [ ] 無效憑證被拒絕並顯示適當錯誤
- [ ] 工作階段在頁面重新載入後持續
- [ ] 登出清除工作階段
回歸 Evals
- [ ] 公開路由仍可存取
- [ ] API 回應未變更
- [ ] 資料庫 schema 相容
### 階段 2實作視情況而定
[撰寫程式碼]
### 階段 3評估
執行:/eval check add-authentication
### 階段 4報告
EVAL 報告add-authentication
==============================
能力5/5 通過pass@3100%
回歸3/3 通過pass^3100%
狀態:準備發佈
```

View File

@@ -0,0 +1,631 @@
---
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
}
```
**記住**:現代前端模式能實現可維護、高效能的使用者介面。選擇符合你專案複雜度的模式。

View File

@@ -0,0 +1,673 @@
---
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 程式碼應該以最好的方式無聊 - 可預測、一致且易於理解。有疑慮時,保持簡單。

View File

@@ -0,0 +1,710 @@
---
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}'
```
**記住**:測試是文件。它們展示你的程式碼應該如何使用。清楚地撰寫並保持更新。

View File

@@ -0,0 +1,202 @@
---
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 個循環,然後繼續 │
└─────────────────────────────────────────────┘
```
### 階段 1DISPATCH
初始廣泛查詢以收集候選檔案:
```javascript
// 從高層意圖開始
const initialQuery = {
patterns: ['src/**/*.ts', 'lib/**/*.ts'],
keywords: ['authentication', 'user', 'session'],
excludes: ['*.test.ts', '*.spec.ts']
};
// 派遣到檢索 agent
const candidates = await retrieveFiles(initialQuery);
```
### 階段 2EVALUATE
評估檢索內容的相關性:
```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**:不相關,排除
### 階段 3REFINE
基於評估更新搜尋標準:
```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)
};
}
```
### 階段 4LOOP
以精煉標準重複(最多 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;
}
```
## 實際範例
### 範例 1Bug 修復上下文
```
任務:「修復認證 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 定義

View File

@@ -0,0 +1,146 @@
---
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 授權)*

View File

@@ -0,0 +1,345 @@
# 專案指南技能(範例)
這是專案特定技能的範例。使用此作為你自己專案的範本。
基於真實生產應用程式:[Zenith](https://zenith.chat) - AI 驅動的客戶探索平台。
---
## 何時使用
在處理專案特定設計時參考此技能。專案技能包含:
- 架構概覽
- 檔案結構
- 程式碼模式
- 測試要求
- 部署工作流程
---
## 架構概覽
**技術堆疊:**
- **前端**Next.js 15App Router、TypeScript、React
- **後端**FastAPIPython、Pydantic 模型
- **資料庫**SupabasePostgreSQL
- **AI**Claude API 帶工具呼叫和結構化輸出
- **部署**Google Cloud Run
- **測試**PlaywrightE2E、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)
```
### 自訂 HooksReact
```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/` - 測試驅動開發方法論

View File

@@ -0,0 +1,494 @@
---
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 SecuritySupabase
```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)
---
**記住**:安全性不是可選的。一個漏洞可能危及整個平台。有疑慮時,選擇謹慎的做法。

View File

@@ -0,0 +1,361 @@
| 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 政策可能危及你的整個基礎設施。總是遵循最小權限原則和深度防禦。

View File

@@ -0,0 +1,63 @@
---
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` 腳本在 PreToolUseEdit/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 - 用於壓縮後存活的狀態

View File

@@ -0,0 +1,409 @@
---
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
---
**記住**:測試不是可選的。它們是實現自信重構、快速開發和生產可靠性的安全網。

View File

@@ -0,0 +1,120 @@
# 驗證循環技能
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
```
報告所有型別錯誤。繼續前修復關鍵錯誤。
### 階段 3Lint 檢查
```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 立即捕捉問題;此技能提供全面審查。