package routes import ( "bytes" "context" "io" "net/http" "time" "github.com/gin-gonic/gin" ) const ( anthropicEventLoggingURL = "https://api.anthropic.com/api/event_logging/batch" eventLoggingForwardTimeout = 8 * time.Second claudeCodeGrowthBookDateUpdated = "1970-01-01T00:00:00Z" ) // RegisterCommonRoutes 注册通用路由(健康检查、状态等) func RegisterCommonRoutes(r *gin.Engine) { // 健康检查 r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Claude Code 遥测日志:清理敏感字段后转发给 Anthropic。 // 删除 baseUrl/gateway 字段防止网关地址暴露(见 FINGERPRINT_SECURITY_REPORT.md §GAP-1/2)。 // 转发而非丢弃,避免"高流量零遥测"异常被检测。 r.POST("/api/event_logging/batch", func(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil || len(body) == 0 { c.Status(http.StatusOK) return } sanitized := sanitizeEventBatch(body) ctx, cancel := context.WithTimeout(c.Request.Context(), eventLoggingForwardTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, anthropicEventLoggingURL, bytes.NewReader(sanitized)) if err != nil { c.Status(http.StatusOK) return } req.Header.Set("Content-Type", "application/json") // 透传客户端的 Authorization header(OAuth Bearer token) if auth := c.GetHeader("Authorization"); auth != "" { req.Header.Set("Authorization", auth) } resp, err := http.DefaultClient.Do(req) if err == nil { resp.Body.Close() } c.Status(http.StatusOK) }) // Claude Code 启动预检:本地 CLI 会在启动早期请求该端点。 r.GET("/api/claude_cli/bootstrap", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "client_data": nil, "additional_model_options": []any{}, "additional_model_costs": gin.H{}, }) }) // Claude Code 组织级策略限制:源码 schema 为 { restrictions: { key: { allowed: boolean } } }。 // 空对象表示当前没有下发任何限制。 r.GET("/api/claude_code/policy_limits", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "restrictions": gin.H{}, }) }) // GrowthBook 特性拉取:真实 Claude Code 远端评估会命中 /api/eval/:clientKey, // SDK 也支持 /api/features/:clientKey,并通过 x-sse-support 探测是否可订阅 SSE。 r.GET("/api/features/:clientKey", func(c *gin.Context) { c.Header("x-sse-support", "enabled") c.JSON(http.StatusOK, gin.H{ "features": gin.H{}, "dateUpdated": claudeCodeGrowthBookDateUpdated, }) }) r.POST("/api/eval/:clientKey", func(c *gin.Context) { c.Header("x-sse-support", "enabled") c.JSON(http.StatusOK, gin.H{ "features": gin.H{}, "dateUpdated": claudeCodeGrowthBookDateUpdated, }) }) writeGrowthBookSSE := func(c *gin.Context) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") c.Status(http.StatusOK) c.SSEvent("features", gin.H{ "features": gin.H{}, "dateUpdated": claudeCodeGrowthBookDateUpdated, }) if flusher, ok := c.Writer.(http.Flusher); ok { flusher.Flush() } } // 真实 Claude Code SDK 使用 /sub/:clientKey 订阅特性更新。 r.GET("/sub/:clientKey", writeGrowthBookSSE) // 兼容当前内部 bootstrap 预热器仍在使用的旧路径,避免本地联调时 404。 r.GET("/sub/features/:clientKey", writeGrowthBookSSE) // Setup status endpoint (always returns needs_setup: false in normal mode) // This is used by the frontend to detect when the service has restarted after setup r.GET("/setup/status", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 0, "data": gin.H{ "needs_setup": false, "step": "completed", }, }) }) }