125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
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",
|
||
},
|
||
})
|
||
})
|
||
}
|