// Package router 装配 Gin 统一接入层的路由与中间件。 package router import ( "os" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sundynix/sundynix-gateway/internal/blob" "github.com/sundynix/sundynix-gateway/internal/handler" "github.com/sundynix/sundynix-gateway/internal/middleware" "github.com/sundynix/sundynix-gateway/internal/nats" "github.com/sundynix/sundynix-gateway/internal/store" ) // New 构建带有 Guardrail / 限流中间件的 Gin 引擎。 func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.Store) *gin.Engine { r := gin.New() r.Use(gin.Recovery()) // panic 兜底 r.Use(middleware.RequestID()) // 生成/透传 X-Request-ID(日志关联) r.Use(middleware.Observe()) // Prometheus 指标 + 结构化访问日志(替代 gin 默认文本日志) r.Use(cors()) // 桌面端/浏览器跨源访问 r.Use(middleware.RateLimit(cache)) r.Use(middleware.Auth()) // 解析 Bearer JWT,注入已验证 userID(非阻断) r.Use(middleware.Guardrail()) // Harness: Input Guardrail h := handler.New(db, cache, bus, blobStore) // 可观测性根端点:Prometheus 抓取 + k8s 存活/就绪探针(不挂业务中间件鉴权)。 r.GET("/metrics", gin.WrapH(promhttp.Handler())) r.GET("/healthz", h.Healthz) r.GET("/readyz", h.Readyz) api := r.Group("/api/v1") { // —— 公开:鉴权端点 / 健康 / 按 task_id 寻址的 SSE 与导出(EventSource/下载无法带 Bearer)—— api.POST("/auth/register", h.Register) // 注册 + 签发 JWT api.POST("/auth/login", h.Login) // 登录 + 签发 JWT api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401) api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯) api.GET("/tasks/:id/stream", h.StreamTask) // SSE 回流 Token Stream(task_id 寻址) api.GET("/tasks/:id/exec", h.StreamExec) // SSE 回流执行轨迹(task_id 寻址) api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(job_id 寻址) api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址) api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx) // —— 受保护:owner 作用域业务,必须携带有效 JWT —— p := api.Group("", middleware.RequireAuth()) { p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid) p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert) p.GET("/kb/list", h.KbList) // 当前用户的知识库列表(owner 隔离) p.POST("/kb/create", h.KbCreate) // 新建知识库 p.POST("/kb/ingest", h.KbIngest) // 文本入库 p.POST("/kb/ingest_file", h.KbIngestFile) // 文件入库 p.POST("/kb/search", h.KbSearch) // 检索台 p.GET("/kb/vault", h.KbVault) // 文库列表 p.GET("/kb/doc", h.KbDoc) // 取单篇文档 p.GET("/kb/links", h.KbLinks) // 某库双链 p.POST("/kb/note", h.KbSaveNote) // 新建/编辑笔记 p.GET("/kb/graph", h.KbGraph) // 知识图谱三元组 p.GET("/agents", h.AgentList) // 我的编排列表(owner 隔离) p.POST("/agents", h.AgentSave) // 保存/更新编排 p.DELETE("/agents", h.AgentDelete) // 删除编排 p.POST("/reports", h.GenerateReport) // 报告生成 p.GET("/billing", h.Billing) } // 运维控制面:LLM 模型配置(含 API 密钥管理)—— 必须管理员(RequireAdmin)。 admin := api.Group("/admin", middleware.RequireAdmin()) { admin.GET("/models", h.ListModels) admin.POST("/models", h.SaveModel) admin.POST("/models/:id/active", h.SetActiveModel) admin.DELETE("/models/:id", h.DeleteModel) admin.POST("/models/test", h.TestModel) admin.GET("/pricing", h.ListPricing) // 各模型计价(token↔真钱) admin.PUT("/pricing", h.SavePricing) // 设置某模型输入/输出单价 } } return r } // cors 控制跨源访问。允许来源经 CORS_ALLOW_ORIGIN 配置(缺省 "*" 仅供开发; // 生产应设为具体源,如 https://app.example.com)。Vary 保证按 Origin 正确缓存。 func cors() gin.HandlerFunc { origin := "*" if v := os.Getenv("CORS_ALLOW_ORIGIN"); v != "" { origin = v } return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", origin) if origin != "*" { c.Header("Vary", "Origin") } c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-ID, X-User-ID") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } }