package mcp import ( "net/http" "sync" "time" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http" "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" ) // RateLimiter 是一对 token bucket:per-token + 全局。 // // 实现选择:固定时间窗口 + 计数器(每分钟重置)。简单、无第三方依赖、 // 内存占用极低。10–50 人规模 + 默认 100 req/min/token 不需要更精细的 // sliding window / leaky bucket。 // // 并发:sync.Mutex + map;rate-limit 调用是热路径(每请求一次),但 mutex // 持有时间极短(map lookup + 1 个 if)。 type RateLimiter struct { cfg RateLimitConfig mu sync.Mutex bucket map[string]*tokenBucket global *tokenBucket now func() time.Time // 测试注入 } // RateLimitConfig 是 limiter 的配置。 type RateLimitConfig struct { PerTokenPerMinute int GlobalPerMinute int } type tokenBucket struct { count int windowAt time.Time } // NewRateLimiter 用配置构造 limiter;now=nil 时用 time.Now。 func NewRateLimiter(cfg RateLimitConfig, now func() time.Time) *RateLimiter { if now == nil { now = time.Now } return &RateLimiter{ cfg: cfg, bucket: map[string]*tokenBucket{}, global: &tokenBucket{}, now: now, } } // Allow 返回 (allowed, reason)。reason 仅在 !allowed 时有值, // 取值 ∈ {"per_token", "global"} 用于错误消息区分。 func (l *RateLimiter) Allow(tokenID string) (bool, string) { l.mu.Lock() defer l.mu.Unlock() now := l.now() // per-token b := l.bucket[tokenID] if b == nil { b = &tokenBucket{} l.bucket[tokenID] = b } if !sameMinute(b.windowAt, now) { b.windowAt = now b.count = 0 } if b.count >= l.cfg.PerTokenPerMinute { return false, "per_token" } // global if !sameMinute(l.global.windowAt, now) { l.global.windowAt = now l.global.count = 0 } if l.global.count >= l.cfg.GlobalPerMinute { return false, "global" } // 两层桶都通过:增量 b.count++ l.global.count++ return true, "" } // sameMinute 判定两时间是否在同一分钟窗口内。 func sameMinute(a, b time.Time) bool { return a.Truncate(time.Minute).Equal(b.Truncate(time.Minute)) } // NewRateLimitMiddleware 必须挂载在 NewAuthMiddleware 之后。 // 从 ctx 取 AuthSession.TokenID 作为 bucket key;找不到 = middleware 顺序错, // 返 500(避免 fail-open 让无鉴权请求通过 rate limit)。 func NewRateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess, ok := FromContext(r.Context()) if !ok { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeInternal, "rate limit middleware: missing auth session (middleware order bug)")) return } allowed, reason := limiter.Allow(sess.TokenID) if !allowed { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeMcpRateLimited, "rate limit exceeded ("+reason+")")) return } next.ServeHTTP(w, r) }) } }